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 type | C type | Notes |
|---|---|---|
int | int64_t | |
float | double | |
bool | bool | |
intc | int | The C-int bridge. Use it whenever C wants an int/unsigned. |
ptr | void* | Opaque handles - a CURL*, a FILE*, a cv::Mat*. |
str | const 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.
