C function interface
Rationale
Programs may need to interact with native libraries or system APIs accessible via code written in C. Examples include database drivers, networking libraries, and low-level I/O bindings. A Foreign Function Interface (FFI) allows a program written in one programming language to call functions written in another. In Coal, this is achieved through the C function interface.
Note that a C function call is not an ordinary Coal expression. It is a low-level construct that bridges the language's own type system and runtime representation with the C function's ABI.
Important!
This language feature is primarily intended for library authors. When exposing a C function to Coal, the programmer is responsible for ensuring that each declaration agrees with the corresponding C function. Application code should generally use existing Coal libraries rather than declare C functions directly.
The syntax to specify a C function call is:
#{<c_function_name : t>}(<arg_0>, <arg_1>, ..., <arg_n>)(<continuation>)
This expression has three parts:
-
The first part is the C function to call. Note that, unlike other expressions in Coal, the type annotation is mandatory here.
#{c_function_name : t} -
The second part is a list of arguments to pass to the C function.
(<arg_0>, <arg_1>, ..., <arg_n>) -
The last component is the continuation. After the C function returns, the generated code passes its result to this expression.
(<continuation>)
C function type
The type declaration's arity must match the number of arguments, and its argument and result types determine the C function signature. The compiler checks the type annotation against the Coal arguments and continuation, but it cannot verify that the annotation matches the actual C function signature.
Type conversions at the C boundary
The type annotation t in #{name : t} must be the full function type. It determines the ABI: how arguments are passed and how the result is returned.
Coal uses a uniform boxed representation, rt_value_t (void *), for values passed to C. However, arguments and return values are handled differently.
Arguments
Each argument is passed as a single void *. The C function must recover the value using the appropriate runtime function.
For example, a function annotated int32 -> double has the C prototype:
double f(void *arg0);
The argument representations and corresponding unboxing functions are listed below.
| Coal type | Representation | Recover in C |
|---|---|---|
bool |
Untagged immediate (0 / 1) |
rt_bool_unbox(a) → bool |
char |
Untagged immediate (Unicode code point) | rt_char_unbox(a) → uint32_t |
int32 |
Untagged immediate | rt_int32_unbox(a) → int32_t |
int64 |
Untagged immediate | rt_int64_unbox(a) → int64_t |
float |
Heap pointer to rt_float_t |
rt_float_unbox(a) → float |
double |
Heap pointer to rt_double_t |
rt_double_unbox(a) → double |
integer |
Heap pointer to rt_bignum_t |
rt_bignum_value(rt_bignum_unbox(a)) → mpz_ptr |
string |
Heap pointer to rt_string_t |
rt_string_data(rt_string_unbox(a)) → char * |
unit |
NULL |
Ignore |
An untagged immediate encodes the value directly in the pointer's numeric value. The runtime's rt_int32_box, rt_int64_box, rt_bool_box, and rt_char_box functions use integer-to-pointer casts without allocating memory. For example, an int32 argument can be recovered with (int32_t)(intptr_t)a.
By contrast, float and double are heap-allocated boxes, so their arguments are actual pointers to runtime objects. Values of other types, such as records, functions, and data constructors, are passed as opaque pointers to their runtime representations.
Return values
The C function returns a value corresponding to the annotated result type. The generated code boxes primitive results where necessary before passing them to the continuation.
| Coal result type | C return type |
|---|---|
bool |
_Bool |
char |
uint32_t (Unicode code point) |
int32 |
int32_t |
int64 |
int64_t |
float |
float |
double |
double |
integer |
rt_bignum_t * |
string |
rt_string_t * |
unit |
void * (usually NULL) |
Primitive values must be returned unboxed. In particular, a C function returning float or double must return the actual floating-point value, not a pointer to a boxed value.
For integer and string, the returned pointer is already the representation expected by Coal. Return an rt_bignum_t * or rt_string_t * created by the runtime, for example with rt_bignum_new(...) or rt_string_new(...). Do not return an mpz_t * or a bare char * in their place.
The continuation is applied to the result after it has been converted to the representation expected by Coal. The continuation's result type is also the type of the entire #{...}(...)(...) expression.
Ownership and mutation
Coal values are managed by the runtime's garbage collector. Follow these rules when working with them in C:
- Do not free Coal values. Do not call
freeon a value passed to C. - Use runtime allocation functions. Return Coal objects created by the runtime, such as
rt_string_newandrt_bignum_new, rather than manually allocating their representations withmalloc. - Treat arguments as borrowed. Do not retain pointers to runtime objects or their internal data beyond the lifetime guaranteed by the runtime.
- Do not mutate Coal values. Coal values are immutable. Do not modify string bytes or bignum contents through pointers returned by
rt_string_dataorrt_bignum_value.
The pointers returned by rt_string_data and rt_bignum_value refer to data held by runtime objects. Their validity depends on the lifetime of those objects.
Example
This Coal expression calls a C function that adds two integers:
#{my_add : int32 -> int32 -> int32}(x, y)(fn(r) => r)#{my_add : int32 -> int32 -> int32}(x, y)(fn(r) => r)
The corresponding C function is:
#include <stdint.h>
int32_t my_add(void *a, void *b)
{
int32_t x = (int32_t)(intptr_t) a;
int32_t y = (int32_t)(intptr_t) b;
return x + y;
}
The int32 arguments are untagged immediates, so their integer values are recovered using intptr_t casts. Defined in <stdint.h>, intptr_t is an optional integer type capable of representing converted pointer values. This technique relies on Coal's runtime representation; it is not a general way to convert arbitrary pointers into integers.
The compiler cannot verify that the annotation matches the actual C function signature. The programmer is responsible for ensuring that they agree.
Compiling C code
Pass each C source file to the compiler using --extra-c. For example:
coal Main.coal --extra-c my_c_lib.c -o dist
For project-based builds, specify the C source files in the c_sources field of coal.json:
{
"c_sources": [
"my_c_lib.c"
]
}