Language
Calling C
Available since Burn
26.1.0-experimental-7.
Native executables can call C functions directly, and C code can call Burn functions. A function marked @Extern is
the bridge in both directions:
import "std/memory"
@Extern("strlen")
fun strlen(s: int): int
@Extern("sqrt")
fun cSqrt(x: float): float
unsafe {
var p = Memory.alloc(16)
Memory.store8(p, 104)
Memory.store8(p + 1, 105)
print(strlen(p), cSqrt(2.0)) // 2 1.4142135623730951
Memory.free(p)
}Calling a C function
@Extern("name") on a top-level function without a body declares a C function: calls to it call the C symbol
name. Without an argument, the symbol is the function's own name.
- Types. Parameters and the result can be
int(a 64-bit integer), a sized integer such asInt32orUInt8,float(a Cdouble) orbool, and the result can bevoid. C knows nothing about Burn's strings, arrays or structs, so pass memory as anintaddress, for example a block fromstd/memory. - Results. A sized integer comes back the way C returns it and is widened like the type says: an
Int32result of-1is-1, aUInt8result keeps only its lowest 8 bits. - Registers. At most 6 integer and 8 float parameters, which is what C passes in registers on every platform
Burn supports. Variadic functions such as
printfare not supported. - Return type. It must be written,
: voidwhen there is none. - Unsafe. Nothing checks what a C function does with memory, so calling one, or using it as a value, needs unsafe code.
The C function has to be linked into the executable. libc and the math library always are, since native Burn programs link against them.
Letting C call Burn
@Extern("name") on a function with a body makes it visible to C under the symbol name, in addition to being an
ordinary Burn function. The same types are allowed. C finds its arguments where it put them, and the function keeps
the registers C expects to survive a call.
A comparator for C's qsort is a typical use:
import "std/memory"
@Extern("qsort")
fun qsort(base: int, n: int, size: int, cmp: int): void
@Extern("compare_ints")
fun compareInts(a: int, b: int): Int32 {
var x = 0
var y = 0
unsafe {
x = Memory.load64(a)
y = Memory.load64(b)
}
if (x < y) {
return -1
}
if (x > y) {
return 1
}
return 0
}C takes the address of a function, which Burn gets from inline assembly:
unsafe {
var cmp = 0
asm {
x86_64 {
lea rax, [rip + compare_ints]
mov {cmp}, rax
}
arm64 {
adrp x9, compare_ints
add x9, x9, :lo12:compare_ints
str x9, {cmp}
}
}
qsort(xs, 5, 8, cmp)
}On macOS, C symbols start with an underscore. Burn adds it for you in @Extern, so write the name as C code
would. In assembly, write the symbol as the macOS assembler expects it: lea rax, [rip + _compare_ints] on x86-64,
and adrp x9, _compare_ints@PAGE followed by add x9, x9, _compare_ints@PAGEOFF on arm64. Because an asm section
cannot tell Linux from macOS, a program that has to build on both can let a line of C hand it the address instead:
void *compare_ints_address(void) { return compare_ints; }, declared in Burn as
@Extern("compare_ints_address") fun compareIntsAddress(): int.
On each target
| Target | @Extern without a body |
@Extern with a body |
|---|---|---|
| native executables | calls the C function | also visible to C under its symbol |
bvm (burn app.bn, burni, .bvmc) |
calling it is a runtime error that names the C function | an ordinary function |
| JavaScript | calling it is a runtime error | an ordinary function |
So a program that only calls C on native targets still runs everywhere, like an asm block
without a section for the target.
Under the hood
Burn functions pass their arguments on the stack. For a C function, the compiler writes a small piece of code that
loads the arguments into the registers the C calling convention uses (rdi, rsi, … and xmm0, … on x86-64,
x0, … and d0, … on arm64), aligns the stack, calls the symbol and widens the result. For a Burn function C can
call, it writes the opposite: code that saves the registers C expects to keep, pushes C's register arguments in
Burn's order and calls the Burn function.
Burn uses the same mechanism for its own runtime and bvm, which are written in Burn and call the C library this way.