System libraries and C FFI

Linking Hare programs with C libraries requires the following steps:

  1. Forward declare necessary C symbols in your Hare sources

  2. Pass the appropriate -l/-L flags to hare build (or hare run, hare test, etc)

Note

Linking to any C libraries will also implicitly link to libc, which will use a libc-oriented Hare runtime that, for example, performs memory allocations via libc’s malloc. This will also enable the +libc build tag.

Linking to libraries

To link to a system library, pass -lname to hare build to link with libname.a (or libname.so) in the default library path. You can add to the library search path with -L (-l and -L are passed directly on to cc by the build driver).

It is recommended to identify the correct linker flags with pkg-config. hare(1) does not interpret C compiler flags, so the user must take care to only add the compatible linker flags:

hare build $(pkg-config --libs-only-L --libs-only-l example) ...

This will Probably Work™.

Application Binary Interface

Hare uses a superset of the standard C ABI for the platform: [1]

Consult the Hare specification for representations of Hare-exclusive constructs, such as tagged unions or tuples.

Calling C from Hare

The types::c module provides type aliases for the host environment’s standard C types, such as char and ulong.

Any symbols or types you wish to use from Hare code in the libraries of interest must be forward-declared in your Hare module. Consider the following C symbol:

FILE *fopen(const char *pathname, const char *mode);

This should be defined in your Hare code like so:

use types::c;

// See stdio.h
export type FILE = opaque;

// See stdio.h
export @symbol("fopen") fn fopen(
     pathname: const *c::char,
     mode: const *c::char,
) nullable *FILE;

The use of @symbol is requried here to ensure that the Hare symbol uses the same symbol name as the C symbol, so that if this appears, for example, in the “libc” Hare namespace, it’s compiled as fopen rather than libc.fopen.

You can then use this function from Hare like so:

let file = fopen(c::nulstr("example.txt\0"), c::nulstr("r\0"));

Warning

Hare strings are not NUL terminated and are not directly compatible with C’s char * type. types::c provides functions to assist in converting between C strings and Hare strings. c::nulstr is useful for string literals which the programmer manually NUL-terminates; otherwise c::fromstr will heap-allocate a NUL-terminated copy of the input string.

Converting from C strings to Hare strings is an O(n) operation that does not require allocation; see c::tostr.

Calling Hare from C

Hare functions which are limited to the C ABI (using types::c or only the C-compatible subset of Hare types) may be directly called by C if you write a header with an appropriate forward declaration. For example, consider the following Hare function:

use types::c;

export fn greet_user(user: const *c::char) int = {
     const user = c::tostr(user)!;
     return fmt::printfln("Hello, {}!", user)!: int;
};

One may call this from C like so:

int greet_user(const char *user);

int main() {
     greet_user("Harriet");
     return 0;
}

See Mixing Hare and C sources for details on getting these two functions to co-exist in a binary.

Note

Most Hare types are intuitively compatible with the C types you would assume, such as u8 being compatible with stdint.h’s uint8_t. Most structs are trivially compatible if you rewrite the syntax appropriately, and tuples are syntax sugar over structs. The internal representations of slices and strings are also given by types::slice and types::string respectively. If in doubt, consult the Hare specification, which defines the internal representation of all Hare types.

On the other hand, if the Hare API knows that its target audience is C users, it can exclusively use types from types::c to limit itself to the trivially compatible ABI subset.

Regarding Hare namespaces

Note that if the “greet_user” function appears in a Hare module (e.g. greetings/user.ha), the symbol name as it appears to C will not be “greet_user”. You must update either the Hare declaration or the C declaration accordingly. To cause Hare to emit a symbol named “greet_user”, use the following code:

export @symbol("greet_user") fn greet_user(user: const *c::char) int = {
     // ...
};

On the other hand, if it is more appropriate for the C code to include the Hare namespace, you may do this with a GCC extension:

int greet_user(const char *user) asm ("greetings.greet_user");

stdarg.h and va_list

Hare supports (type unsafe) C-style variadism using stdarg.h-compatible semantics. To call a variadic C function from Hare, simply define it with ... in the variadic parameter position.

export @symbol("printf") fn printf(format: const *c::char, ...) int;

You can call this function normally from Hare code.

To implement a variadic function that can be called from C, or to prepare a va_list that can be passed to a C function, you can define your own Hare function with the ... operator and use the valist type and vastart, vaarg, and vaend expressions.

use fmt;
use strings;
use types::c;

// Implements a C-compatible printf function
export @symbol("printf") fn printf(format: const *c::char, ...) int = {
     const fmt = c::tostr(format);
     const iter = strings::iter(fmt);

     const ap: valist = vastart();
     defer vaend(ap);

     for (true) {
             const next = match (strings::next(&iter)) {
             case void => break;
             case let rn: rune =>
                     yield rn;
             };
             if (rn != '%') {
                     fmt::print(rn)!;
                     continue;
             };

             const ctrl = strings::next(&iter) as rune;
             switch (ctrl) {
             case 'd' =>
                     fmt::print(vaarg(ap): int)!;
             case 's' =>
                     const s: *c::char = vaarg(ap);
                     fmt::print(c::tostr(s))!;
             // ...
             };
     };
};

Mixing Hare and C sources

There are two ways to do this, depending on which tool you want to perform the linking. Note that you can also use any other language (C++, Rust, etc) with either approach so long as the language in question can work with object files; adapt these instructions as necessary.

If you want hare(1) to perform linking

Using a separate build orchestrator (e.g. make), compile C sources to object files with cc -c [...] and place these object files into the Hare module directory where they belong. The build driver will automatically link them with executables that depend on this module.

If you want cc(1) or ld(1) to perform linking

Using a separate build orchestrator (e.g. make), compile Hare sources to object files with hare build -t o [...]. Pass all of the Hare objects to cc(1) or ld(1) when linking.

Note

Note that hare build -t o builds an entire Hare module, not just one source file.

The resulting object file only includes this module, and not any of the Hare dependencies it imports via the use keyword. You must enumerate dependencies yourself, build them separately, and pass all of the corresponding objects to the linker.

The hare deps command is useful for enumerating the dependencies of a module or source file.

Footnotes