Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

C

Overview

The C target emits the canonical C header and a thin reference C file that every other WeaveFFI target ultimately speaks to. All cross-language bindings sit on top of these symbols, so the C output is also the easiest way to inspect what the IDL compiles to.

What gets generated

FilePurpose
generated/c/weaveffi.hPublic header: opaque types, enums, function prototypes, error/memory helpers
generated/c/weaveffi.cEmpty placeholder for future convenience wrappers (kept so projects can link a single TU if desired)

Type mapping

IDL typeC parameter typeC return type
i32int32_tint32_t
u32uint32_tuint32_t
i64int64_tint64_t
u64uint64_tuint64_t
i8int8_tint8_t
i16int16_tint16_t
u8uint8_tuint8_t
u16uint16_tuint16_t
f32floatfloat
f64doubledouble
boolboolbool
stringconst char* (NUL-terminated UTF-8)const char*
bytesconst uint8_t* ptr, size_t lenconst uint8_t* + size_t* out_len
handleweaveffi_handle_tweaveffi_handle_t
Structconst weaveffi_m_S*weaveffi_m_S*
Enum (plain)weaveffi_m_Eweaveffi_m_E
Enum (rich)const weaveffi_m_E*weaveffi_m_E*
T? (value)const T* (NULL = absent)T* (NULL = absent)
[T]const T* items, size_t items_lenT* + size_t* out_len
iter<T>n/aopaque iterator handle (see Iterators)

C ABI symbol naming follows a strict convention:

KindPatternExample
Functionweaveffi_{module}_{function}weaveffi_contacts_create_contact
Struct typeweaveffi_{module}_{Struct}weaveffi_contacts_Contact
Struct createweaveffi_{module}_{Struct}_createweaveffi_contacts_Contact_create
Struct destroyweaveffi_{module}_{Struct}_destroyweaveffi_contacts_Contact_destroy
Struct getterweaveffi_{module}_{Struct}_get_{field}weaveffi_contacts_Contact_get_name
Enum typeweaveffi_{module}_{Enum}weaveffi_contacts_ContactType
Enum variantweaveffi_{module}_{Enum}_{Variant}weaveffi_contacts_ContactType_Personal
Callback typedefweaveffi_{module}_{Callback}_fnweaveffi_events_OnMessage_fn
Listener registerweaveffi_{module}_register_{listener}weaveffi_events_register_message_listener
Listener unregisterweaveffi_{module}_unregister_{listener}weaveffi_events_unregister_message_listener
Async callbackweaveffi_{module}_{function}_callbackweaveffi_tasks_run_task_callback
Async launcherweaveffi_{module}_{function}_asyncweaveffi_tasks_run_task_async
Iterator typeweaveffi_{module}_{Function}Iteratorweaveffi_events_GetMessagesIterator
Iterator nextweaveffi_{module}_{Function}Iterator_nextweaveffi_events_GetMessagesIterator_next
Iterator destroyweaveffi_{module}_{Function}Iterator_destroyweaveffi_events_GetMessagesIterator_destroy

{Function} is the function name converted to PascalCase (get_messagesGetMessages).

When the IDL sets c_prefix, every symbol, including the runtime helpers, is rewritten with the new prefix.

Example IDL → generated code

version: "0.4.0"
modules:
  - name: contacts
    enums:
      - name: ContactType
        variants:
          - { name: Personal, value: 0 }
          - { name: Work, value: 1 }
          - { name: Other, value: 2 }

    structs:
      - name: Contact
        fields:
          - { name: name, type: string }
          - { name: email, type: "string?" }
          - { name: age, type: i32 }

    functions:
      - name: create_contact
        params:
          - { name: first_name, type: string }
          - { name: last_name, type: string }
        return: Contact

      - name: find_contact
        params:
          - { name: id, type: "i32?" }
        return: "Contact?"

      - name: list_contacts
        params: []
        return: "[Contact]"

      - name: count_contacts
        params: []
        return: i32

The header opens with an include guard, standard headers, an extern "C" block, and the shared error/memory helpers:

#ifndef WEAVEFFI_H
#define WEAVEFFI_H

#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>

#ifdef __cplusplus
extern "C" {
#endif

typedef uint64_t weaveffi_handle_t;

typedef struct weaveffi_error {
    int32_t code;
    const char* message;
} weaveffi_error;

void weaveffi_error_clear(weaveffi_error* err);
void weaveffi_free_string(const char* ptr);
void weaveffi_free_bytes(uint8_t* ptr, size_t len);

Structs become forward-declared opaque typedefs reached via create/destroy/getter functions:

typedef struct weaveffi_contacts_Contact weaveffi_contacts_Contact;

weaveffi_contacts_Contact* weaveffi_contacts_Contact_create(
    const char* name,
    const char* email,
    int32_t age,
    weaveffi_error* out_err);

void weaveffi_contacts_Contact_destroy(weaveffi_contacts_Contact* ptr);

const char* weaveffi_contacts_Contact_get_name(
    const weaveffi_contacts_Contact* ptr);

Enums turn into typed enum declarations with prefixed variants:

typedef enum {
    weaveffi_contacts_ContactType_Personal = 0,
    weaveffi_contacts_ContactType_Work = 1,
    weaveffi_contacts_ContactType_Other = 2
} weaveffi_contacts_ContactType;

Optionals and lists use pointer-with-sentinel and pointer+length pairs:

int32_t* weaveffi_store_find(const int32_t* id, weaveffi_error* out_err);

weaveffi_contacts_Contact** weaveffi_contacts_list_contacts(
    size_t* out_len,
    weaveffi_error* out_err);

Every function takes a trailing weaveffi_error* out_err. On failure out_err->code is non-zero and out_err->message points at a Rust-allocated string the consumer must clear:

weaveffi_error err = {0, NULL};
int32_t total = weaveffi_contacts_count_contacts(&err);
if (err.code != 0) {
    fprintf(stderr, "Error %d: %s\n", err.code, err.message);
    weaveffi_error_clear(&err);
    return 1;
}

Rich (algebraic) enums

An enum whose variants declare fields is a rich (algebraic) enum, a sum type with associated data. Unlike a plain C-style enum (a bare int32_t discriminant), a rich enum crosses the ABI as an opaque object pointer, exactly like a struct: the producer owns the payload and the consumer holds a handle. A plain _Tag enum names the discriminants, then constructors, a tag reader, per-variant getters, and a destructor operate on the handle. From the shapes sample (Shape = Empty | Circle{radius} | Rectangle{width,height} | Labeled{label,count}):

typedef enum {
    weaveffi_shapes_Shape_Empty = 0,
    weaveffi_shapes_Shape_Circle = 1,
    weaveffi_shapes_Shape_Rectangle = 2,
    weaveffi_shapes_Shape_Labeled = 3
} weaveffi_shapes_Shape_Tag;

typedef struct weaveffi_shapes_Shape weaveffi_shapes_Shape;

int32_t weaveffi_shapes_Shape_tag(const weaveffi_shapes_Shape* self);

weaveffi_shapes_Shape* weaveffi_shapes_Shape_Empty_new(weaveffi_error* out_err);
weaveffi_shapes_Shape* weaveffi_shapes_Shape_Circle_new(double radius, weaveffi_error* out_err);
weaveffi_shapes_Shape* weaveffi_shapes_Shape_Rectangle_new(float width, float height, weaveffi_error* out_err);
weaveffi_shapes_Shape* weaveffi_shapes_Shape_Labeled_new(const char* label, uint8_t count, weaveffi_error* out_err);

double weaveffi_shapes_Shape_Circle_get_radius(const weaveffi_shapes_Shape* self);
float weaveffi_shapes_Shape_Rectangle_get_width(const weaveffi_shapes_Shape* self);
float weaveffi_shapes_Shape_Rectangle_get_height(const weaveffi_shapes_Shape* self);
const char* weaveffi_shapes_Shape_Labeled_get_label(const weaveffi_shapes_Shape* self);
uint8_t weaveffi_shapes_Shape_Labeled_get_count(const weaveffi_shapes_Shape* self);

void weaveffi_shapes_Shape_destroy(weaveffi_shapes_Shape* self);

Read _tag, then call only the matching variant’s getters. A getter that returns a const char* hands back Rust-owned memory to free with weaveffi_free_string:

weaveffi_error err = {0, NULL};
weaveffi_shapes_Shape* shape = weaveffi_shapes_Shape_Circle_new(2.0, &err);

if (weaveffi_shapes_Shape_tag(shape) == weaveffi_shapes_Shape_Circle) {
    printf("radius = %f\n", weaveffi_shapes_Shape_Circle_get_radius(shape));
}

const char* text = weaveffi_shapes_describe(shape, &err);
printf("%s\n", text);
weaveffi_free_string(text);

weaveffi_shapes_Shape_destroy(shape);

The consumer owns every weaveffi_shapes_Shape* returned by a constructor or by a function such as weaveffi_shapes_scale; release each one with weaveffi_shapes_Shape_destroy.

Build instructions

The runnable example uses the calculator sample crate.

macOS:

cargo build -p calculator

cd examples/c
cc -I ../../generated/c main.c -L ../../target/debug -lcalculator -o c_example
DYLD_LIBRARY_PATH=../../target/debug ./c_example

Linux:

cargo build -p calculator

cd examples/c
cc -I ../../generated/c main.c -L ../../target/debug -lcalculator -o c_example
LD_LIBRARY_PATH=../../target/debug ./c_example

Windows:

cargo build -p calculator
cd examples\c
cl /I ..\..\generated\c main.c /link calculator.lib
.\main.exe

See examples/c/main.c for end-to-end usage.

Memory and ownership

Rust always owns memory it allocates. Strings and byte buffers returned across the boundary must be freed by the consumer with the matching helper:

const char* name = weaveffi_contacts_Contact_get_name(contact);
printf("Name: %s\n", name);
weaveffi_free_string(name);

size_t len;
const uint8_t* data = weaveffi_storage_get_data(&len, &err);
weaveffi_free_bytes((uint8_t*)data, len);

For struct handles, call the matching _destroy symbol when the consumer is done. Borrowed parameters (const T*, string/bytes inputs) remain owned by the caller for the duration of the call only.

Callbacks and listeners

A callbacks: entry becomes a function-pointer typedef whose parameters mirror the IDL signature plus a trailing opaque void* context. A listeners: entry becomes a register/unregister pair built on that typedef. From the events sample:

typedef void (*weaveffi_events_OnMessage_fn)(const char* message, void* context);

uint64_t weaveffi_events_register_message_listener(
    weaveffi_events_OnMessage_fn callback,
    void* context);
void weaveffi_events_unregister_message_listener(uint64_t id);

The contract:

  • register_* stores the (callback, context) pair and returns a uint64_t subscription id. Pass that id to unregister_* to stop delivery.
  • context is opaque to the producer and is passed back verbatim as the last argument of every invocation. It must stay valid until the listener is unregistered.
  • The producer invokes the callback on its own thread, whenever the event fires. The callback must be thread-safe and must not assume it runs on the registering thread.
  • Pointer arguments (e.g. const char* message) are only valid for the duration of the invocation; copy anything that must outlive it.
static void on_message(const char* message, void* context) {
    int* count = context;       /* runs on the producer's thread */
    (*count)++;
}

weaveffi_error err = {0, NULL};
int count = 0;
uint64_t id = weaveffi_events_register_message_listener(on_message, &count);
weaveffi_events_send_message("hello", &err);   /* fires the listener */
weaveffi_events_unregister_message_listener(id);

Async support

Async functions (async: true) get no synchronous prototype. Each one emits a per-function callback typedef, (void* context, weaveffi_error* err, <result slots>), and a launcher with the _async suffix. From the async-demo sample:

typedef void (*weaveffi_tasks_run_task_callback)(
    void* context,
    weaveffi_error* err,
    weaveffi_tasks_TaskResult* result);

void weaveffi_tasks_run_task_async(
    const char* name,
    weaveffi_tasks_run_task_callback callback,
    void* context);

The launcher returns immediately; WeaveFFI invokes the callback exactly once, with either a result or a populated error, from the producer’s worker thread.

For cancellable: true functions the launcher gains a weaveffi_cancel_token* slot before the callback, and the runtime provides the token lifecycle (from the kvstore sample, whose function is named compact_async, hence the doubled suffix):

void weaveffi_kv_compact_async_async(
    weaveffi_kv_Store* store,
    weaveffi_cancel_token* cancel_token,
    weaveffi_kv_compact_async_callback callback,
    void* context);

weaveffi_cancel_token* weaveffi_cancel_token_create(void);
void weaveffi_cancel_token_cancel(weaveffi_cancel_token* token);
bool weaveffi_cancel_token_is_cancelled(const weaveffi_cancel_token* token);
void weaveffi_cancel_token_destroy(weaveffi_cancel_token* token);

See Async functions for the full pattern.

Iterators

Functions returning iter<T> produce an opaque iterator handle plus _next/_destroy functions instead of a materialized list. From the events sample (get_messages returns iter<string>):

typedef struct weaveffi_events_GetMessagesIterator weaveffi_events_GetMessagesIterator;

weaveffi_events_GetMessagesIterator* weaveffi_events_get_messages(
    weaveffi_error* out_err);
int32_t weaveffi_events_GetMessagesIterator_next(
    weaveffi_events_GetMessagesIterator* iter,
    const char** out_item,
    weaveffi_error* out_err);
void weaveffi_events_GetMessagesIterator_destroy(
    weaveffi_events_GetMessagesIterator* iter);

_next writes the next element into the one-slot out-param and returns 1, or returns 0 when exhausted (leaving *out_item untouched). Failures are reported through out_err. Element ownership follows the usual return rules; here each const char* must be freed with weaveffi_free_string. Always call _destroy when done, even if iteration stopped early:

weaveffi_error err = {0, NULL};
weaveffi_events_GetMessagesIterator* iter = weaveffi_events_get_messages(&err);
const char* item = NULL;
while (weaveffi_events_GetMessagesIterator_next(iter, &item, &err) == 1) {
    printf("%s\n", item);
    weaveffi_free_string(item);
}
weaveffi_events_GetMessagesIterator_destroy(iter);

Troubleshooting

  • undefined reference to weaveffi_*: make sure the linker sees the cdylib (-L target/debug -l<your-crate>). The header alone is not enough.
  • Crashes inside weaveffi_free_string: the pointer wasn’t Rust-allocated. Only free pointers returned from a generated getter or function.
  • error: unknown type weaveffi_handle_t: the consumer included the header without <stdint.h>. Include order matters; the generated header pulls in the standard integer typedefs explicitly.
  • weaveffi.c is empty: that file is intentionally a placeholder. All declarations live in weaveffi.h.