Mica has no FFI, in either direction:
- Every call from Mica to C is a direct C ABI call.
- Every call from C to Mica is a direct C ABI call.
No wrapper, no glue and no marshaling layer stands between the two languages. A Mica value is laid out the way the C ABI lays out its C counterpart, and a Mica routine is called the way a C function is called, so the two link and call each other as two C files would. The rules that keep the boundary safe live inside the Mica routine or in the runtime, never between the two, and this page states every one a C developer meets there. The tutorials walk the two directions with a running example each: Mica calls C and C calls Mica.
Mica calls C
A C library’s surface is declared once, in its contract, and a C function is then an imported name whose every call the compiler checks against the declared signature. The call it emits is the plain native call the C ABI defines, with the arguments in the registers and stack slots a C caller would use.
A value that a C function answers or hands to a callback, typed in the
contract as an enumeration, a subrange, bool or unicode, is checked
against its type where it enters Mica, and a value outside it stops the
program with ordinal_out_of_domain.
C calls Mica
An exported routine of a Mica library links under the build target’s name,
two underscores, and the exported name, with __utf8 behind it in a library
built for UTF-8: exp function Fibonacci of a library Utilities is
Utilities__Fibonacci. --emit-header writes the C header that declares every
export under that name, with every type the exports use.
An exported routine keeps the C convention on its side of the call: a record parameter it only reads arrives by value, as C passes it, and an eight- or sixteen-bit argument is read at its own width, whatever the C caller left in the bits above it.
A Mica program may export routines too, and a C library it calls may call them back through their linkage names while the program runs.
What crosses, and how
| Mica | C |
|---|---|
int8 to int64, uint8 to uint64 | int8_t to int64_t, uint8_t to uint64_t |
float32, float64 | float, double |
bool | bool |
unicode | char32_t |
| an enumeration | a type name over its base integer, its literals as constants |
| a subrange | its base integer |
| a record | a struct of the same fields in the same order |
| a packed record | a __attribute__((packed)) struct |
| a fixed array behind a pointer | an array type |
pointer T; a const parameter of that type | T *; const T * |
string | the descriptor MicaStringView |
A value crosses in one of three modes, Mica’s own: by value, where the
callee gets a copy; by pointer, address at the call, where the callee may
write through it; and by const pointer, where the callee may only read, and
the compiler holds a Mica callee to that. A record answered by value travels in
registers or through the hidden result address, exactly where the C ABI puts
it.
An export C cannot express, such as a fixed array passed or answered by value, a task, or a generic routine, is left out of the generated header, and the header’s opening comment names it with the reason.
Strings and the encodings
A Mica string crosses as two machine words, the address of its first code
unit and its length in characters:
typedef struct MicaStringView
{
const mica_char_t *Data;
uint64_t Length;
} MicaStringView;A program or library built for UTF-32 has wchar_t as its code unit, and one
built for UTF-8 has char, one byte of its text. The length counts characters
under both encodings. A zero code unit follows the last character, so C’s text
functions read the text where it stands, and the empty string may carry no
address at all.
A C function whose contract takes UTF-8 text is called with the text as it stands from a program built for UTF-8. A program built for UTF-32 writes the text as UTF-8 into a scratch block of the runtime just before the call and releases it after the call, and the call itself is the same direct C ABI call.
A C function’s text result, declared c_text in its contract, is copied into
a string of the program’s own at every call, decoded for the program’s
encoding: each ill-formed byte sequence becomes one replacement character, and
a null pointer becomes the empty string. The C side keeps its own buffer.
Who owns a cell that crosses
Memory crosses the boundary with its owner, and the owner releases it:
- What a Mica library hands your C code, the library releases. Give it an
exported routine that releases what it handed out, and call that routine,
never
free:freeknows only the blocksmallocmade. - Memory your C code allocated stays yours to release. Hand it to a Mica
library to read or to fill, never to release: the compiler refuses a
Disposeof a pointer a C function answered, also when another unit receives it, and a checked build stops at such a release withforeign_pointer_releasewhere no rule can see it ahead of time. - A
borrowed_pointerin a contract carries no ownership: the cell belongs to the other side, and Mica demands no release for it. - A string or a record an exported routine answers is released by its caller when the caller is Mica, and a string answered to a C caller still reads as it did after the same thread’s next call.
In the fixed-arena memory class every Mica cell comes from one block of static storage, which a C program may define itself, and the rules above are the same.
Callbacks
A Mica routine handed to C is an ordinary C function pointer, which C calls at
the place its contract names. A callback routine is declared with the C shape
of its slot and bound by name at the call: C calls it while the imported call
runs, at a during_call slot, and its answer steers the C side as a C
callback’s would. The contract’s callback slots
state the class, the C signature and the userdata parameter.
The compiler refuses, each in its own words: a generic callback, which has no single C shape; a callback that can fail, since C does not read Mica’s failure channel; a callback that reaches into an enclosing routine’s frame; a callback called as a routine or used as a value; a plain routine at a callback slot; a shape that disagrees with the slot’s C signature; and an exported callback.
A C routine a library calls may call the library’s own exports before it
returns. The compiler reads every such call as a call of each export, and a
routine whose contract states "effects": {"never_calls_back": true} is read as
returning only by returning.
Stored callbacks and callbacks from C’s own threads
A slot whose contract class is registered stores the callback and fires it
after the call returns, possibly from threads the C library started. A library
built with --tasking multicore binds a callback there: each thread that fires
it becomes a Mica task of its own, as every C thread that calls into Mica does
(see threads in C), so the callback guards what it shares with
synchronized. Here a C library keeps Tick and fires it from four threads of
its own after Start returned, and the count keeps every firing:
{ A tick adds one to the count under its own mark, whichever thread fires it. }
callback Tick(context : pointer int64);
begin
synchronized ticks := ticks + 1;
end;
{ A start hands the tick to the C library, which keeps it and fires it later. }
exp procedure Start();
begin
StartTicker(Tick, address ticks);
end;The context the C library hands back to every firing outlives the call, so a context that points into the calling routine’s frame, the address of one of its locals, is refused by name: hand it a unit variable’s address or memory the program keeps alive. A library built for one thread refuses the binding by name, because it keeps C’s rule of one thread in Mica code at a time.
A during_call callback may be fired from threads the C function starts
while the call runs, and on the multicore floor each firing thread is a task
of its own as well, so the callback’s synchronized statement keeps every
firing.
Heapless routines
A routine declared heapless promises to use no heap, and the compiler holds
it to the promise: every construct that allocates is refused at its own site,
and a call of a C function is admitted only where the function’s contract
states heapless, the attribute a Mica library’s emitted contract writes for
its own heapless routines. A heapless callback handed to such a function is
checked like any heapless routine.
A checked build keeps the promise while the program runs, through every road
the compiler cannot see: the first allocation or release a heapless routine
reaches, inside its C callees too, stops the program and names the routine and
what it reached, a C allocator entry such as malloc, or the program’s fixed
arena. A C program that links a checked library is watched over the C
allocator’s entries in the same way; its own fixed arena is not watched.
Threads in C
A C thread calls a Mica library the way it calls any C library: directly, with
no call to make first. The program calls the library’s body entry once, before
its first call into the library (see the library’s entries),
and no thread makes a call of its own beforehand. Build the library with --tasking multicore, link it
with the multicore runtime floor mica-dragon-rt-mc-<arch>.a, and each C
thread becomes a Mica task of its own the first time its call reaches the
runtime, with its own failure record, its own region for the strings a routine
returns and its own heap partition. The task goes back when its thread ends, as
the thread leaves its start routine or calls pthread_exit.
No lock is taken for the call, and calls from several threads run at the same time.
A thread’s first call allocates nothing, a heapless routine’s call among them.
The library keeps sixteen task records ready from the moment it loads, and a
thread of its own maps sixteen more whenever the threads find the reserve low,
so a library serves any number of threads; a thread that finds the reserve
empty sleeps until a record is free. The library also makes, as it loads, the
thread key that gives a thread’s task back, so the key stays among the first
thirty-two, which the GNU C Library keeps inside every thread. A shared object
a program loads with dlopen after the program made thirty-two keys of its own
is the exception: the C library then allocates once in each new thread’s first
call.
A signal handler the library installs may run on a thread that has no task, a thread of the host that never called the library among them. It runs on a record lent for the handler’s duration, and the thread leaves the handler with no task again.
Such a library is exactly as thread-safe as its own code, as a C library is.
State it keeps between calls, a unit variable among it, is guarded with
synchronized, and in a library built for several threads a synchronized
statement always takes its lock, so it keeps the host’s threads apart. This
library counts every addition of eight C threads, and the host takes no lock:
library Tally;
var
count : int64;
{ A bump adds one to the count round after round, under the routine's own mark. }
exp procedure Bump(rounds : int64);
var
round : int64;
begin
for round := 1 to rounds do
synchronized count := count + 1;
end;
{ The count is read after the host joined its threads. }
exp function Count() : int64;
begin
Count := count;
end;
begin
count := 0;
end.#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
extern void Tally__init(void);
extern void Tally__Bump(int64_t rounds);
extern int64_t Tally__Count(void);
static void *worker(void *unused) {
(void)unused;
Tally__Bump(100000);
return NULL;
}
int main(void) {
pthread_t threads[8];
Tally__init();
for (int i = 0; i < 8; i++) pthread_create(&threads[i], NULL, worker, NULL);
for (int i = 0; i < 8; i++) pthread_join(threads[i], NULL);
printf("%lld of 800000\n", (long long)Tally__Count());
return 0;
}The library is built for UTF-32, so its names carry no __utf8, from a
mica.project that states its version. The host, main.c, runs the library’s
body once, links it with the standard library and the multicore floor, and
prints 800000 of 800000:
mica --compile --link --output-modifier archive --optimize O2 \
--platform linux,arm64,utf-32 --tasking multicore --project mica.project \
--source Tally.mica --build build
cc -pthread -o tally main.c build/Tally.a /usr/lib/mica-stdlib-arm64.a /usr/lib/mica-dragon-rt-mc-arm64.a -lm
./tallyA routine that places tasks, a concurrent block among them, runs them on the
library’s pool of carriers while the calling thread sleeps, and returns only
after every task it placed has ended. A thread that enters the library again
through a C function the library called stays the same task, and the inner
call’s tasks end before the outer call goes on.
A library built with --tasking single keeps C’s rule for a library that is
not thread-safe: one thread runs Mica code at a time, and the host makes its
threads take turns with a mutex of its own:
static pthread_mutex_t tally_turn = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&tally_turn);
Tally__Bump(100000);
pthread_mutex_unlock(&tally_turn);Such a library links with the single-carrier floor mica-dragon-rt-<arch>.a,
and a library built with --tasking multicore links with the multicore floor;
a link that pairs a library with the other floor is refused by name, on either
road. A Mica program whose --tasking differs from a library it imports stops
before the linker runs:
Failed to link: library 'Tally' was compiled for one carrier: a program built with '--tasking multicore' imports it built the same way
Failed to link: library 'Tally' was compiled for '--tasking multicore': a program that imports it is built with the same '--tasking'A C program is refused by its own linker, because a library’s objects name a marker that only the floor of their tasking defines. A multicore library linked with the single-carrier floor names the first marker, and a single-carrier library linked with the multicore floor the second:
...: hidden symbol `__mica_floor_tasking_multicore' isn't defined
...: hidden symbol `__mica_floor_tasking_single' isn't definedThe generated header states each exported routine’s thread safety on the
multicore floor in the words of the GNU C Library’s manual: MT-Safe where the
routine touches no state of the library outside a synchronized statement, and
MT-Unsafe with a race word for each state it touches without one, as in
MT-Unsafe race:count. A routine that trains reads race:tape, since the
process has one gradient tape, so two C threads train one at a time. The
header states as well which runtime the library was built for. A multicore
library’s header says that each calling thread becomes a task of its own on its
first call and that several threads may call the library at once; a
single-carrier library’s header says that a program calling the library from
several threads makes them take turns, holding one lock of its own around every
call.
A failure that leaves an exported routine from inside a synchronized
statement gives the statement’s lock back on its way out, so the next thread’s
call proceeds.
A thread that ends while it is inside a Mica call, by pthread_exit from a
callback or by cancellation, leaves the program undefined, as the GNU C
Library’s manual says of a function it marks AC-Unsafe. Let a callback return
to its caller, and keep cancellation disabled across a call into Mica with
pthread_setcancelstate.
How a failure reaches C
A trap inside a library ends the process the way it ends a Mica program: one
Mica runtime failure line on standard error naming the reason, the file,
line and column of the library’s source where it happened, and exit status 1.
A trap in the library’s initialization stops the process there, before the
body of a Mica program that imports the library runs.
A routine declared with fails reports its failure on Mica’s failure channel,
which a Mica caller reads and C does not: the generated header leaves such a
routine out and says why, and a call through a declaration of the C program’s
own returns to C after a failure, with the lock of every synchronized
statement it left given back. A
library meant for C reports an outcome C must read in its result or through a
pointer parameter.
The library’s entries
Every library has one more entry, the target’s name and __init
(Learner__init__utf8 for a library Learner built for UTF-8), which the
generated header declares. A C program calls it once, before its first call
into the library: it runs the body’s statements and gives each variable that
starts at a value other than zero its first value. A library whose body is
empty and whose variables start at zero returns from it at once, so a program
that calls it keeps working when the body later gains a statement. A Mica
program that imports the library calls the entry before its own first
statement wherever the entry has work to do.
A library that places work on processes declares one more entry in its
header, the target’s name and __init_processes. The C program calls it once,
early in main and before it starts threads of its own, and the library’s
placements then run on the process server it starts; a second call returns at
once. A call while a second thread runs, or a placement without the call, ends
the program with words that name the entry.
What an archive and a shared object export
A static archive publishes the library’s exp-marked declarations and nothing
else: no private routine, no generic template and none of the release
machinery the compiler writes.
A shared object carries a copy of the runtime floor, and one process holds one
floor whatever it loads: a second Mica shared object, or a program, binds to
the floor already there, so one scheduler serves them all. Loading leaves the
process as the C program made it, its locale and its signal handlers among it,
and dlclose gives back what the object holds.
A library that exports a task, or that places work on processes, is refused as a shared object by name before any tool runs, and links as an archive.