Dragon

Calling C and C++

Dragon does not live on an island. Decades of battle-tested native libraries already exist - libcurl, SQLite, zlib, OpenCV, your company's in-house SDK - and Dragon talks to all of them through its foreign-function interface.

The good news is how little ceremony it takes. For a C library you write no C at all: you declare the functions you want in .dr and the linker wires them up. For a C++ library you write one tiny `extern "C" shim - because C++ name-mangling has no stable ABI - and dragon build` compiles it for you. That's the whole spectrum.

This chapter is about declaring and calling. The two companion chapters finish the story: Marshalling and intc is how data crosses the boundary, and Linking Native Libraries is how you point the build at the code.

The two ways to declare a foreign function

extern "C" introduces a function whose body lives in native code. It comes in two forms:

# Form 1 - bind a named library. Auto-links `-lcurl` at build time.
extern "C" from "curl" {
    def curl_easy_init() -> ptr
    def curl_easy_perform(handle: ptr) -> intc
    def curl_easy_cleanup(handle: ptr)
}

# Form 2 - a symbol the linker already sees (libc, libm, or a `-l` lib).
extern "C" def sqrt(x: float) -> float

ptr is the type of a raw pointer - void* at the C ABI level - and is your escape hatch for any opaque handle a library hands back. Once declared, a foreign function is called exactly like a Dragon function:

extern "C" def sqrt(x: float) -> float

print(sqrt(2.0))        # 1.4142135623730951

This is not a toy. Dragon's own standard library is built this way: stdlib/math.dr wraps libm with nothing but extern "C" def lines, and stdlib/threading.dr binds raw pthread_mutex_* directly.

How Dragon values map to C

Because Dragon values flow at their native machine types, a bound call is a direct native call - there is no marshalling layer to pay for.

Dragon typeC typeNotes
intint64_t
floatdouble
boolbool
intcintThe C-int bridge. Use it whenever C wants an int/unsigned.
ptrvoid*Opaque handles - a CURL*, a FILE*, a cv::Mat*.
strconst char*A pointer to the text buffer. Valid as a C string for ASCII - see below.
bytes(uint8_t* data, int64_t len)The vehicle for binary data and non-ASCII text.

The first five - int, float, bool, intc, ptr - are the trivial core: any C function whose signature uses only these binds with a bare declaration and no glue whatsoever. The last two, str and bytes, are where the boundary gets interesting - encoding, NUL termination, and lifetime all matter. That is the subject of Marshalling and intc, and it is worth reading before you pass your first string.

A function built only from the trivial core is the easiest possible binding - here is libc's abs, declared and called with nothing else:

extern "C" def abs(x: intc) -> intc

print(abs(-7))          # 7

Calling C++

You cannot bind a C++ symbol directly: name-mangling turns cv::imread(const std::string&) into something like _ZN2cv6imreadERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEi, and that mangling is not a stable contract. Every language that talks to C++ - Rust, Python, Swift - solves this the same way: a thin extern "C" shim that flattens the C++ surface to plain C entry points. You write that shim once.

// cvshim.cpp - the only C++ you author
#include <opencv2/opencv.hpp>

extern "C" void* cvshim_imread(const char* path) {
    return new cv::Mat(cv::imread(path));
}
extern "C" int  cvshim_rows(void* m) { return static_cast<cv::Mat*>(m)->rows; }
extern "C" int  cvshim_cols(void* m) { return static_cast<cv::Mat*>(m)->cols; }
extern "C" void cvshim_free(void* m) { delete static_cast<cv::Mat*>(m); }
# imgsize.dr
extern "C" from "opencv_core" {
    def cvshim_imread(path: str) -> ptr
    def cvshim_rows(m: ptr) -> intc
    def cvshim_cols(m: ptr) -> intc
    def cvshim_free(m: ptr)
}

def size(path: str) -> str {
    m: ptr = cvshim_imread(path)
    dims: str = str(cvshim_cols(m)) + "x" + str(cvshim_rows(m))
    cvshim_free(m)
    return dims
}

print(size("photo.png"))

The C++ side is plain pointer-passing: the shim news a cv::Mat, hands Dragon back an opaque ptr, and frees it on request. Dragon never looks inside. How you hand that shim to the build - and why the linker has to switch from cc to c++ to do it - is covered in Linking Native Libraries.

The bigger picture

Dragon's philosophy is to do everything it can in Dragon, and to reach for C only when a capability is genuinely native - a kernel that's been hand-tuned for years, a vendor SDK, raw hardware. The FFI is the seam that makes that practical: wrap the irreducible native core in a thin shim, then build the ergonomic, type-safe, Dragon-speed API on top of it in .dr. That is exactly how a pandas-style dataframe (numeric kernels via BLAS), an OpenCV binding (the C++ shim above), or a fast HTML parser would come together - native muscle, Dragon on the outside.

Next: Marshalling and intc for moving data across the boundary, then Linking Native Libraries for the build flags.