Files
Ivan FBandClaude Opus 5 a9fc67df3f build(wasm)!: bump the edge to nim-ffi 0.3.0, matching master
Closes the last drift from master's 4a85db1b: the edge was still building
against a vendored, threads-off fork of nim-ffi 0.1.3 while everything else had
moved to 0.3.0.

wasm-deps/ffi is re-vendored from the pinned 0.3.0 with a much smaller patch
than the 0.1.3 one: rather than gating ~20 call sites (and re-gating them on
every bump), ffi_singlethread.nim supplies API-compatible no-ops for
ThreadSignalPtr and Thread, which are what --threads:off actually forbids. The
upstream lifecycle code then compiles untouched. The only behavioural change is
sendRequestToFFIThread, which runs the handler on the caller's chronos loop
instead of enqueuing it for a worker that is never started.

edge_lib.nim moves to the 0.3.0 surface: declareLibrary now takes the library
type, contexts come from the generated <LibType>FFIPool, and genBindings()
closes the file. The five request procs use `{.ffiRaw: "abi = c".}` — the one
annotation that keeps the explicit (ctx, callback, userData, ...cstring) C
signature the browser already calls, so edge_new / edge_lightpush_publish /
edge_filter_subscribe / edge_store_* / edge_stop keep their symbols and arity.

Two things 0.3.0 changes that ARE visible, hence the `!`:

- Request replies are CBOR text strings, not raw bytes. `abi = c` does not
  prevent it: ffiRaw expands to registerReqFFI, which pins the codec to CBOR.
  Short replies (a hash, "") looked fine; only storeQuery's JSON.parse caught
  it. Hosts must strip the 1-5 byte header — the demos and ld-edge.js do, and
  fall through for unframed replies so they still work against a 0.1.x binary.
- FFIContext lost eventCallback/eventUserData in favour of a listener registry.
  logosdeliveryedge_set_event_callback keeps its exported signature and stores
  into a module-level slot instead, so events stay unframed and the JS is
  unchanged. A browser edge node has exactly one context.

Request params must be `string`, not `cstring`: the request is CBOR-encoded, so
a cstring field encodes the pointer and the multiaddr arrives empty.

Verified against the Status staging fleet with lion-signet's selftest — 20
passed, 0 failed, 0 skipped: connect, lightpush v3, store query + paging,
byte-identical payload round trip, history decrypt+verify, ns timestamps as
strings, offline invite recovery, clean teardown.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012qDYE5r2t2dMry5XpWWySu
2026-08-08 04:23:13 +02:00
..
2026-06-25 09:27:01 +02:00

Logos Messaging API (LMAPI) Library

A C FFI library providing a simplified interface to Logos Messaging functionality.

Overview

This library wraps the high-level API functions from waku/api/api.nim and exposes them via a C FFI interface, making them accessible from C, C++, and other languages that support C FFI.

The call surface is generated by nim-ffi from the {.ffi.} annotations in library/*.nim. make liblogosdelivery writes it to library/generated/logosdelivery.h on every build, so it can never drift from the Nim signatures. It is a build artifact and is not checked in: build the library before you compile anything against it.

Include library/liblogosdelivery.h, which pulls in the generated header and adds the event-listener ABI.

Every entry point takes the context handle (void *ctx) first, except the constructor. The rest of the signature depends on the call:

  • No-argument calls (start_node, stop_node, get_available_configs, get_available_node_info_ids) take a raw LogosDeliveryScalarRawFn: (void *ctx, LogosDeliveryScalarRawFn cb, void *userData).
  • Argument-taking calls (subscribe, unsubscribe, send, get_node_info) take a per-call LogosDelivery<Name>ReplyFn and pass their arguments last, in a request struct: (void *ctx, LogosDelivery<Name>ReplyFn onReply, void *userData, const <Name>Req *req).

The generator emits one reply typedef per call (e.g. LogosDeliverySubscribeReplyFn), all with the same shape:

typedef void (*LogosDeliveryScalarRawFn)(int callerRet, char *msg, size_t len, void *userData);
typedef void (*LogosDeliverySubscribeReplyFn)(int errCode, const char *reply, const char *errMsg, void *userData);

reply, errMsg and msg are borrowed: copy them if you need them after the callback returns.

API Functions

Node Lifecycle

logosdelivery_create_node

Creates a node from the given configuration JSON.

typedef struct { const char *configJson; } CreateNodeCtorReq;

typedef void (*LogosDeliveryCreateRawFn)(
    int errCode,
    const char *ctxAddr,   // context address as decimal text, on success
    const char *errMsg,
    void *userData
);

void *logosdelivery_create_node(
    const CreateNodeCtorReq *req,
    LogosDeliveryCreateRawFn onCreated,
    void *userData
);

Parameters:

  • req->configJson: JSON string containing node configuration
  • onCreated: Callback that receives the terminal result
  • userData: User data passed to the callback

Returns: the context handle, or NULL on failure. Creation is asynchronous: wait for onCreated before you make any other call.

Example configuration JSON:

{
  "mode": "Core",
  "preset": "logos.dev",
  "messagingOverrides": {
    "listen-address": "0.0.0.0",
    "tcp-port": 60000,
    "discv5-udp-port": 9000
  }
}

The configuration object has four optional top-level keys: mode ("Core" or "Edge", defaults to "Core"), preset, messagingOverrides (per-field node config overrides), and channelsOverrides (reliable-channel overrides). Override keys accept the config field name or its CLI switch name (e.g. "clusterId" or "cluster-id"); unknown keys are rejected. Use "preset" to select a network preset (e.g., "twn", "logos.dev", "status.prod") which auto-configures entry nodes, cluster ID, sharding, and other network-specific settings.

Available presets:

Preset Cluster ID RLN Sharding Network
twn 1 on auto (8 shards) The Waku Network
logos.dev 2 off auto (8 shards) Logos Dev Network
logos.test 2 off auto (8 shards) Logos Test Network
status.prod 16 off auto (1 shard) Status Production Network

logosdelivery_start_node

Starts the node.

int logosdelivery_start_node(void *ctx, LogosDeliveryScalarRawFn callback, void *userData);

logosdelivery_stop_node

Stops the node.

int logosdelivery_stop_node(void *ctx, LogosDeliveryScalarRawFn callback, void *userData);

logosdelivery_destroy

Destroys a node instance and frees resources. This call is synchronous; do not use ctx afterwards.

int logosdelivery_destroy(void *ctx);

Messaging

logosdelivery_subscribe

Subscribe to a content topic to receive messages.

typedef struct { const char *contentTopicStr; } SubscribeReq;

int logosdelivery_subscribe(
    void *ctx,
    LogosDeliverySubscribeReplyFn onReply,
    void *userData,
    const SubscribeReq *req
);

Parameters:

  • ctx: Context handle returned by logosdelivery_create_node
  • req->contentTopicStr: Content topic string (e.g., "/myapp/1/chat/proto")
  • onReply: Callback function to receive the result
  • userData: User data passed to the callback

logosdelivery_unsubscribe

Unsubscribe from a content topic.

typedef struct { const char *contentTopicStr; } UnsubscribeReq;

int logosdelivery_unsubscribe(
    void *ctx,
    LogosDeliveryUnsubscribeReplyFn onReply,
    void *userData,
    const UnsubscribeReq *req
);

logosdelivery_send

Send a message.

typedef struct { const char *messageJson; } SendReq;

int logosdelivery_send(
    void *ctx,
    LogosDeliverySendReplyFn onReply,
    void *userData,
    const SendReq *req
);

Parameters:

  • req->messageJson: JSON string containing the message

Example message JSON:

{
  "contentTopic": "/myapp/1/chat/proto",
  "payload": "SGVsbG8gV29ybGQ=",
  "ephemeral": false
}

Note: The payload field should be base64-encoded.

Returns: Request ID in the callback message that can be used to track message delivery.

Events

Events are delivered through a per-event listener registry: register one callback per event name you care about. A registration returns a listener id you can later pass to remove it.

logosdelivery_add_event_listener

Registers callback for the named event and returns a non-zero listener id (0 on an invalid context).

uint64_t logosdelivery_add_event_listener(
    void *ctx,
    const char *eventName,
    FFICallBack callback,
    void *userData
);

Event names: onMessageSent, onMessageError, onMessagePropagated, onMessageReceived, onConnectionStatusChange, onTopicHealthChange, onConnectionChange, onReceivedMessage, onChannelMessageReceived, onChannelMessageSent, onChannelMessageError.

logosdelivery_remove_event_listener

Removes a previously registered listener. Returns 0 on success, 1 if the listener id was not found or the context is invalid.

int logosdelivery_remove_event_listener(
    void *ctx,
    uint64_t listenerId
);

Important: Callbacks run on a dedicated event thread and should be fast, non-blocking, and thread-safe.

Building

The library follows the same build system as the main Logos Messaging project.

Build the library

make liblogosdeliveryStatic    # Build static library
# or
make liblogosdeliveryDynamic   # Build dynamic library

Return Codes

All functions that return int use the following return codes:

  • NIMFFI_RET_OK / RET_OK (0): Success
  • NIMFFI_RET_ERR / RET_ERR (1): Error
  • NIMFFI_RET_MISSING_CALLBACK / RET_MISSING_CALLBACK (2): Missing callback function
  • NIMFFI_RET_STALE_WARN (3): Non-terminal progress tick, always followed by a terminal code. Ignore it unless you want progress.

Callback Functions

Results come back through one of four callback shapes. The generated names carry the library prefix (LogosDelivery); the reply typedef is emitted once per call.

// Argument-taking calls: one typedef per call, all this shape.
typedef void (*LogosDeliverySubscribeReplyFn)(
    int errCode,
    const char *reply,
    const char *errMsg,
    void *userData
);

// No-argument calls (start/stop/get_available_*).
typedef void (*LogosDeliveryScalarRawFn)(
    int callerRet,
    char *msg,
    size_t len,
    void *userData
);

// Constructor.
typedef void (*LogosDeliveryCreateRawFn)(
    int errCode,
    const char *ctxAddr,
    const char *errMsg,
    void *userData
);

// Event listeners (declared by liblogosdelivery.h, not the generated header).
typedef void (*FFICallBack)(
    int callerRet,
    const char *msg,
    size_t len,
    void *userData
);
  • Reply typedefs (LogosDelivery<Name>ReplyFn): reply is the result on success (NUL-terminated, may be empty); errMsg is the message on failure.
  • LogosDeliveryScalarRawFn and FFICallBack: msg holds len bytes and is not NUL-terminated.
  • LogosDeliveryCreateRawFn: ctxAddr is the context address as decimal text on success.

All of these strings are borrowed and valid only for the duration of the call. Copy them if you need them afterwards.

Example Usage

#include "liblogosdelivery.h"
#include <stdio.h>
#include <unistd.h>

static volatile int created = -1;
static void *node = NULL;

// The argument-taking calls share this reply shape.
void on_reply(int ret, const char *reply, const char *errMsg, void *userData) {
    if (ret == RET_OK) {
        printf("Success: %s\n", reply ? reply : "");
    } else {
        printf("Error: %s\n", errMsg ? errMsg : "unknown error");
    }
}

// The no-argument calls (start/stop) take the raw callback.
void on_scalar(int ret, char *msg, size_t len, void *userData) {
    if (ret == RET_STALE_WARN) return;  // progress tick, ignore
    printf("%.*s\n", (int)len, msg ? msg : "");
}

void on_created(int ret, const char *ctxAddr, const char *errMsg, void *userData) {
    created = (ret == RET_OK);
}

int main() {
    const char *config = "{"
        "\"mode\": \"Core\","
        "\"preset\": \"logos.dev\""
        "}";

    // Create the node. The return value is the context handle; wait for
    // on_created before making any other call.
    CreateNodeCtorReq createReq = { .configJson = config };
    node = logosdelivery_create_node(&createReq, on_created, NULL);
    for (int i = 0; i < 100 && created == -1; i++) {
        usleep(100000);
    }
    if (created != 1 || node == NULL) {
        return 1;
    }

    // Start node
    logosdelivery_start_node(node, on_scalar, NULL);

    // Subscribe to a topic
    SubscribeReq subReq = { .contentTopicStr = "/myapp/1/chat/proto" };
    logosdelivery_subscribe(node, on_reply, NULL, &subReq);

    // Send a message
    const char *msg = "{"
        "\"contentTopic\": \"/myapp/1/chat/proto\","
        "\"payload\": \"SGVsbG8gV29ybGQ=\","
        "\"ephemeral\": false"
        "}";
    SendReq sendReq = { .messageJson = msg };
    logosdelivery_send(node, on_reply, NULL, &sendReq);

    // Clean up. logosdelivery_destroy is synchronous.
    logosdelivery_stop_node(node, on_scalar, NULL);
    logosdelivery_destroy(node);

    return 0;
}

Architecture

The library is structured as follows:

  • liblogosdelivery.h: Public C header; includes the generated header and adds the event ABI
  • generated/logosdelivery.h: Generated call surface, emitted by make liblogosdelivery (not checked in)
  • liblogosdelivery.nim: Main library entry point
  • declare_lib.nim: Library declaration and initialization
  • logos_delivery_api/node_api.nim: Node lifecycle API implementation
  • logos_delivery_api/messaging_api.nim: Subscribe/send API implementation

The library uses the nim-ffi framework for FFI infrastructure, which handles:

  • Thread-safe request processing
  • Async operation management
  • Memory management between C and Nim
  • Callback marshaling

See Also

  • Main API documentation: waku/api/api.nim
  • Original libwaku library: library/libwaku.nim
  • nim-ffi framework: vendor/nim-ffi/