Freestanding Hare programs
You can build Hare programs that do not make use of the standard library, for use-cases such as bootloaders, kernels, executable payloads, and so on.
Hare programs built under these conditions target the “freestanding” environment, which is distinguished from the “hosted” environment by the following traits:
The user must provide a compatible Hare runtime implementation
The standard library is not available in the freestanding environment
The entry point may or may not be “main”, and may have a different signature than prescribed for the hosted environment
It is possible to provide an alternate runtime or standard library, customize the layout of ELF executables or use another format entirely, and so on. It is not currently possible to change the calling convention, but you can provide entry points in assembly or C which perform the necessary translation.
Building in the freestanding environment
You will need to provide a HAREPATH
root which includes, at a minimum, the
rt
module. All Hare builds implicitly link with rt
, and you can provide
your startup code, entry point, and anything else necessary for your target
environment here.
Note
The build driver also pulls in the debug
module by default. You can pass
-R to build in release mode, or provide a debug module.
To run the build driver in freestanding mode, set HAREPATH
to include only
the path to your custom Hare library root and pass -F
to hare build
to
enable freestanding mode.
Minimum requirements
You must provide, at a minimum, a suitable Hare runtime in your environment.
Implementing rt
The Hare runtime generally provides the program’s entry point, if applicable, and implements runtime functionality depended on by the compiler. The upstream Hare compiler (harec) will emit calls to the following runtime functions, which you must implement.
Note
You will likely want to add assembly sources to your runtime. See Linking with assembly code for details.
The compiler expects rt to provide the following functions:
// Copy n bytes from src to dest. The memory areas shall not overlap.
export fn memcpy(dest: *opaque, src: *const opaque, n: size) void;
// Copy n bytes from src to dest. The memory areas may overlap.
export fn memmove(dest: *opaque, src: *const opaque, n: size) void;
// Set n bytes of dest to val.
export fn memset(dest: *opaque, val: u8, n: size) void;
// Returns true if both strings are equal.
export fn strcmp(a: str, b: str) bool;
// Terminate the program with an error message.
export @symbol("rt.abort") fn _abort(
path: *str,
line: u64,
col: u64,
msg: str,
) never;
const fixed_errors: [_]str = [
"slice or array access out of bounds", // 0
"type assertion failed", // 1
"out of memory", // 2
"static insert/append exceeds slice capacity", // 3
"execution reached unreachable code (compiler bug)", // 4
"slice allocation capacity smaller than initializer", // 5
"assertion failed", // 6
"error occurred", // 7
];
// Terminate the program due to one of a fixed list of runtime error
// conditions. "i" is the index of the list of error conditions provided
// above.
export fn abort_fixed(path: *str, line: u64, col: u64, i: u64) void;
If you wish to implement a heap (for use with alloc, free, and non-static append, insert, and delete operations), the runtime must also provide the following functions:
// Allocates at least n bytes of memory and returns a pointer to the
// allocated memory, or null if unable to allocate sufficient memory.
export fn malloc(n: size) nullable *opaque;
// Frees memory previously allocated with [[malloc]].
export @symbol("rt.free") fn _free(p: nullable *opaque) void;
// The following runtime functions are used for dynamic slice operations:
type slice = struct {
data: nullable *opaque,
length: size,
capacity: size,
};
// Called on "append" and "insert". The runtime should ensure that the given
// slice's underlying memory is at least its capacity times membsz bytes in
// length.
export fn ensure(s: *slice, membsz: size) void;
// Called on "delete". The runtime may reclaim memory from the slice's
// underlying storage if the capacity exceeds the length.
export fn unensure(s: *slice, membsz: size) void;
The runtime is also responsible for calling @init functions on startup and @fini functions on exit, if appropriate. The following Hare declarations will forward-declare the init and fini arrays for use in your entry point:
const @symbol("__init_array_start") init_start: [*]*fn() void;
const @symbol("__init_array_end") init_end: [*]*fn() void;
const @symbol("__fini_array_start") fini_start: [*]*fn() void;
const @symbol("__fini_array_end") fini_end: [*]*fn() void;
Custom linker scripts
You can add a custom linker script if you wish at rt/hare.sc
. The build
driver will consider build tags, so you may for instance provide different
rt/hare+x86_64.sc
and rt/hare+aarch64.sc
scripts. You can use this
linker script to customize the output format, arrangement and address of
sections, and so on.
Most Hare distributions use the binutils linker, or lld, which is compatible with GNU linker scripts. GNU linker scripts are documented here: Linker script reference
Implementing debug
When building in debug mode, the build driver will automatically link to the “debug” module.
The debug module is not expected to have any particular behavior associated with it, and the Hare compiler does not use it directly. In the hosted library, the debug module uses @init to set up additional debugging features through hooks in rt.
How any additional runtime debugging features work (or don’t) in your custom Hare environment is up to you. The simplest approach is simply to make an empty module with a placeholder:
// This is a placeholder so that running "hare build" in debug mode works.
def PLACEHOLDER = true;
Examples of freestanding programs
freestanding-linux is a simple freestanding Hare program which builds a “hello world” program for x86_64 Linux without using the upstream standard library or runtime.