SDKs and platforms

How do I call Offline Protocol from C, C++, Go or C#?

Offline Protocol ships a C API, offline_protocol.h, over the same native library the other bindings load. You create an engine from a JSON configuration, call any SDK method by name with JSON parameters, and receive results and events as JSON. Any language that can call C uses it the same way, including C++, Go through cgo, C# through P/Invoke and Zig.

TL;DR

The C API is the SDK's local API called in process, about a dozen C functions that take and return JSON. The header and shared libraries ship in each GitHub release's desktop archives for Linux, macOS and Windows. C++, Go, C# and Zig bind the same functions, and the host supplies transports and storage.

Learning objectives

After reading this article you will be able to:

  • Get offline_protocol.h and the shared library for Linux, macOS or Windows and link a C program
  • Call SDK methods by name with JSON parameters and handle results, errors and events
  • Plan a wrapper for C++, Go, C# or Zig and know what the host must supply

Offline Protocol C API

The Offline Protocol C API is the SDK’s local API called in process. Instead of one C function per SDK method, it gives you about a dozen functions: offline_protocol_create builds an engine from a JSON configuration, offline_protocol_call calls any method of the interface definition by name with a JSON object of parameters, and results and events come back as JSON text.

That shape was chosen for languages other than C. A typed C API would be about 220 functions and 46 structures with nested ownership. A JSON surface stays the same size as the SDK grows, because the dispatch is generated from the interface definition, so a Go or C# package written against these functions keeps working across releases. Your program reads results and events with the JSON library it already uses.

Header and shared libraries per OS

The header and the shared library come in every desktop archive on a GitHub release; the header is also in bindings/c/include in the SDK repository.

OSShared library
Linux x86_64 and aarch64liboffline_protocol_uniffi.so
macOS arm64liboffline_protocol_uniffi.dylib
Windows x86_64offline_protocol_uniffi.dll, with its import library offline_protocol_uniffi.dll.lib

The iOS XCFramework in the same release carries the same C functions under the same header. The Android libraries do not: the C binding is off in the Android builds.

Linking a C program against the shared library takes one line:

cc -I include app.c -L <libdir> -loffline_protocol_uniffi -Wl,-rpath,<libdir>

Releases do not ship a static archive, which is over ten times the size of the shared library. To build one, along with the shared library, from source:

cargo build --release -p offline-protocol-uniffi
# target/release/liboffline_protocol_uniffi.{so,dylib,a}
# (the static archive may land in target/release/deps/)

Linking it needs the system libraries rustc names, which this prints:

cargo rustc --release -p offline-protocol-uniffi --lib -- --print native-static-libs

Calling SDK methods with JSON

Method names, parameter names and the JSON form of every type are the local API’s, so its method table is the reference for what you can call. A send from the C quick start looks like this:

  if (offline_protocol_call(engine, "send_message",
          "{\"recipient\":\"off1...\",\"content\":\"hi\",\"priority\":\"Medium\"}",
          &result, &err) == OfflineProtocolStatus_Ok) {
    printf("queued %s\n", result); /* the message id */
    offline_protocol_string_free(result);
  } else {
    fprintf(stderr, "send: %s (%d) %s\n", err.variant ? err.variant : "-", err.code, err.message);
    offline_protocol_error_clear(&err);
  }

The naming rules are short:

  • Engine methods are bare (send_message). MeshServices methods carry services. and DataStore methods carry data..
  • Parameters are a JSON object keyed by the definition’s parameter names; NULL means none. An unknown key is refused, and an omitted field takes the definition’s default.
  • Bytes are padded standard base64, and enums are spelled as the definition spells them, such as "WiFiDirect" or "Medium".
  • An engine refusal sets err.variant to the error’s variant name and err.code to its position.

Every string the library returns is freed with offline_protocol_string_free, and nothing else is. Every entry point catches a panic and answers OfflineProtocolStatus_Internal instead of aborting your process. A few declarations take a callback and have their own functions: the event callback, the four transport notifications, initialize_mls with your own storage providers, and the storage conformance suite.

C++, Go, C# and Zig

Any language that calls C uses the API the same way: a thin wrapper binds the few functions and passes JSON strings.

  • C++. Include the header directly; it compiles as C++. Catch exceptions inside callbacks, because a callback must not unwind into the library.
  • Go. Call the functions through cgo. There is also the community generator uniffi-bindgen-go, in a release that targets the UniFFI version the SDK pins; it binds UniFFI’s own ABI rather than the C API.
  • C#. Declare the functions for P/Invoke and load the shared library for the host OS.
  • Zig. Import the header and link the library like any C library.

The same contract applies in each. A callback runs synchronously on whichever thread is inside the engine, must be safe from any thread and must not call back into the engine. Every function that takes a callback also takes a drop function and a ctx, and the library calls drop(ctx) exactly once, even when it refuses the registration, so the host never frees ctx itself. Instead of a callback you can poll with offline_protocol_poll_event; the queue keeps the newest 1,000 events.

Java has its own typed binding that calls the library through the foreign function API instead, and Rust can use the crates directly.

Host responsibilities with the C API

With the C API your process is the platform: the engine handles routing, encryption and delivery, and your code owns the radios, sockets and files:

  • Transports. Drain each transport’s outbound queue through the ble_, internet_, wifi_direct_, reticulum_ and nostr_ methods, perform the I/O, report the outcome and inject what you receive. The transport callbacks say when a queue has frames; the internet transport has none and is polled.
  • Storage. Write two storage providers in C, or use the built-in file stores through initialize_mls_with_file_stores, which needs no callbacks. Prove a provider with offline_protocol_run_storage_conformance.
  • Lifecycle. Call start, stop, and process on a timer. For telemetry the binding fills os, and you pass os_major and report backgrounding with notify_app_state.
  • One engine per handle, freed once, with no call in flight when it is.

For transports ready-made on desktop and server hosts, the Python package ships the bridges. A program in any language can also share one engine run by the Python package’s offline-protocol-service, over JSON-RPC 2.0 on a WebSocket, or call a service on another device with an ordinary HTTP request.

C binding testing in CI

The C API is checked from C on every change. CI compiles two complete programs with -std=c99 -Wpedantic -Werror and runs them: main.c exercises the contract with a storage provider written in C against both the shared and the static library, and exchange.c runs two engines with the host carrying frames between them and forming an MLS session. The quick start in the README is extracted, compiled as written and run on Linux and macOS, and on Windows under cl -W4 -WX. The two programs also run under AddressSanitizer and UBSan, so a string freed twice or with the wrong free is reported where it happened.

Before a release publishes, it builds the quick start beside each shipped library and header, with the archive’s own link line, on macOS, Linux and Windows. The SDK is AGPL-3.0-only, with a commercial licence for proprietary use.

Frequently asked questions

How do Go and C# programs use Offline Protocol?

Through the C API. A Go program binds its few functions with cgo and a C# program with P/Invoke, then passes JSON strings. For Go there is also the community generator uniffi-bindgen-go, which binds UniFFI's own ABI instead.

Does the C API break when the SDK adds a method?

No. A new method in the interface definition is callable by name through offline_protocol_call with no change to the C signatures, so a wrapper written against these functions keeps working across releases.

Can I link Offline Protocol statically?

Yes, if you build it yourself. Releases ship the shared library only, because the static archive is over ten times its size. cargo build --release -p offline-protocol-uniffi produces it, and rustc prints the system libraries to link with it.

Does the C API work on Android?

No. The C binding is off in the Android builds. The iOS XCFramework in the release carries the same C functions, and the same header applies.

Who frees the strings the C API returns?

Your code, with offline_protocol_string_free and nothing else. Free a result only after a call returned OfflineProtocolStatus_Ok, and clear an error with offline_protocol_error_clear after every failure.

Sources

Build it with Offline Protocol

The C page of the docs has the files you need, the full quick start, the rules for ownership and threads, and what your host provides.

Read the C binding docs