This guide is for application developers embedding the moss shared library into an existing product.
Use this together with docs/API.md:
API.mdis the exact exported FFI surface- this document is the practical integration guide: packaging, lifecycle, memory ownership, callbacks, and JNI patterns
cmd/moss-ffi builds a native shared library plus a generated C header:
# Linux
go build -buildmode=c-shared -o libmoss.so ./cmd/moss-ffi
# Windows
go build -buildmode=c-shared -o moss.dll ./cmd/moss-ffi
# macOS Intel
CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 \
go build -buildmode=c-shared -o libmoss.dylib ./cmd/moss-ffi
# macOS Apple Silicon
CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 \
go build -buildmode=c-shared -o libmoss.dylib ./cmd/moss-ffiBuild outputs:
- Windows:
moss.dllandmoss.h - Linux/macOS:
libmoss.soorlibmoss.dylibandlibmoss.h
The generated header is the source of truth for types and callback signatures.
Typical host lifecycle:
- Load the native library.
- Register global keystore callbacks if you want persistent identity.
- Call
Moss_Init(meshId, psk, configJson). - Register message and event callbacks on the returned handle.
- Call
Moss_Start(handle). - Subscribe to channels with
Moss_Subscribe. - Publish with
Moss_Publish. - Call
Moss_Stop(handle)during shutdown.
Recommended call order:
Moss_SetKeyStore(...) optional, global
Moss_Init(...)
Moss_SetCallback(...)
Moss_SetEventCallback(...)
Moss_SetScoringCallback(...) optional, per-handle
Moss_Start(...)
Moss_Subscribe(...)
Moss_Publish(...)
Moss_Stop(...)
These rules are the most important part of a correct integration.
Host-owned memory:
mesh_idpskconfig- publish payload buffers passed into
Moss_Publish
Moss-owned memory returned to the host:
Moss_GetMeshInfoMoss_GetPublicKeyMoss_GetNATType
Anything returned by those functions must be released with:
Moss_Free(ptr);Never free Moss-owned memory with:
freedeleteMarshal.FreeHGlobalctypesmanual deallocatorJNI Release*functions
Always use Moss_Free.
Callbacks are invoked from Moss runtime goroutines. Do not assume they run on your UI thread.
Practical rule:
- treat all callbacks as background-thread callbacks
- copy any incoming data you need
- forward work onto your application event loop / main thread yourself
This matters for:
- Java UI frameworks
- Android main looper
- .NET UI frameworks
- Python UI frameworks
- game engines
Do not do heavy blocking work directly inside callbacks.
Moss_Init accepts a JSON config string. The most common host pattern is:
- keep your app config in your own native format
- render only the Moss-relevant subset to JSON
- pass that JSON into
Moss_Init
Example:
{
"listen_port": 41030,
"trackers": [
"udp://tracker.opentrackr.org:1337/announce"
],
"static_peers": [],
"gossipsub": {
"heartbeat_ms": 1000
},
"nat": {
"upnp_enabled": true,
"natpmp_enabled": true,
"pcp_enabled": true
}
}Notes:
- omit
trackersto use the built-in default tracker set - pass
"trackers": []to disable tracker bootstrap explicitly - partial nested objects are supported
Ship:
moss.dll- your application executable
Recommended layout:
MyApp.exe
moss.dll
Ship:
libmoss.so
Recommended options:
- place it next to the executable and set
rpath - or install into a known library directory and set loader paths explicitly
Ship the architecture-correct libmoss.dylib:
- Intel Macs:
darwin/amd64 - Apple Silicon:
darwin/arm64
For production packaging, codesigning and bundle-relative loader paths are the host app's responsibility.
#include "moss.h"
#include <stdio.h>
#include <string.h>
static void on_message(const char* channel,
const uint8_t* sender_id,
const uint8_t* data,
uint32_t len) {
(void)sender_id;
printf("message on %s: %.*s\n", channel, (int)len, (const char*)data);
}
static void on_event(int32_t event_type, const char* detail_json) {
printf("event %d: %s\n", (int)event_type, detail_json);
}
int main(void) {
const char* config = "{\"listen_port\":41030}";
MossHandle handle = Moss_Init("my-mesh", NULL, config);
if (handle < 0) {
return 1;
}
Moss_SetCallback(handle, on_message);
Moss_SetEventCallback(handle, on_event);
Moss_Start(handle);
Moss_Subscribe(handle, "lobby");
const char* text = "hello";
Moss_Publish(handle, "lobby", (const uint8_t*)text, (uint32_t)strlen(text));
Moss_Stop(handle);
return 0;
}For Java, the correct model is:
- load
mossand your JNI bridge library - keep Moss behind a thin native bridge
- convert JNI calls to
Moss_* - forward callbacks back into Java through cached method IDs
Do not call Moss_* directly from Java using raw FFM/JNA/JNR unless you are willing to own native callback complexity and pointer lifetime details. JNI is the safer path for a production integration.
Java/Kotlin app
-> JNI bridge you own
-> moss shared library
Why this is the right boundary:
- Java gets a simple object-oriented API
- your JNI layer owns callback marshaling
- your JNI layer can post callbacks onto the JVM thread/executor you choose
- native handle lifetime stays explicit
Example Java wrapper:
package com.example.moss;
public final class MossNode implements AutoCloseable {
static {
System.loadLibrary("moss_jni");
}
private long handle;
public MossNode(String meshId, String configJson) {
this.handle = nativeInit(meshId, configJson);
if (this.handle <= 0) {
throw new IllegalStateException("Moss init failed: " + this.handle);
}
}
public void start() {
check(nativeStart(handle), "start");
}
public void subscribe(String channel) {
check(nativeSubscribe(handle, channel), "subscribe");
}
public void publish(String channel, byte[] payload) {
check(nativePublish(handle, channel, payload), "publish");
}
public void setListener(MossListener listener) {
nativeSetListener(handle, listener);
}
public String meshInfoJson() {
return nativeGetMeshInfo(handle);
}
@Override
public void close() {
if (handle != 0) {
nativeStop(handle);
handle = 0;
}
}
private static void check(int code, String op) {
if (code != 0) {
throw new IllegalStateException("Moss " + op + " failed: " + code);
}
}
private static native long nativeInit(String meshId, String configJson);
private static native int nativeStart(long handle);
private static native int nativeStop(long handle);
private static native int nativeSubscribe(long handle, String channel);
private static native int nativePublish(long handle, String channel, byte[] payload);
private static native void nativeSetListener(long handle, MossListener listener);
private static native String nativeGetMeshInfo(long handle);
}Listener contract:
package com.example.moss;
public interface MossListener {
void onMessage(String channel, byte[] senderId, byte[] payload);
void onEvent(int eventType, String detailJson);
}At minimum your bridge needs to:
- store
MossHandle - hold a
JavaVM* - keep a global ref to the Java listener
- cache
jmethodIDforonMessageandonEvent - attach callback threads to the JVM before calling back into Java
JNI bridge sketch:
#include <jni.h>
#include <stdint.h>
#include <string.h>
#include "moss.h"
static JavaVM* g_vm = NULL;
static jobject g_listener = NULL;
static jmethodID g_onMessage = NULL;
static jmethodID g_onEvent = NULL;
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
(void)reserved;
g_vm = vm;
return JNI_VERSION_1_8;
}
static JNIEnv* get_env(int* did_attach) {
*did_attach = 0;
JNIEnv* env = NULL;
if ((*g_vm)->GetEnv(g_vm, (void**)&env, JNI_VERSION_1_8) == JNI_OK) {
return env;
}
if ((*g_vm)->AttachCurrentThread(g_vm, (void**)&env, NULL) == JNI_OK) {
*did_attach = 1;
return env;
}
return NULL;
}
static void release_env(int did_attach) {
if (did_attach) {
(*g_vm)->DetachCurrentThread(g_vm);
}
}
static void on_message_cb(const char* channel,
const uint8_t* sender_id,
const uint8_t* data,
uint32_t len) {
int did_attach = 0;
JNIEnv* env = get_env(&did_attach);
if (env == NULL || g_listener == NULL) {
return;
}
jstring jChannel = (*env)->NewStringUTF(env, channel);
jbyteArray jSender = (*env)->NewByteArray(env, 32);
jbyteArray jPayload = (*env)->NewByteArray(env, (jsize)len);
(*env)->SetByteArrayRegion(env, jSender, 0, 32, (const jbyte*)sender_id);
(*env)->SetByteArrayRegion(env, jPayload, 0, (jsize)len, (const jbyte*)data);
(*env)->CallVoidMethod(env, g_listener, g_onMessage, jChannel, jSender, jPayload);
(*env)->DeleteLocalRef(env, jChannel);
(*env)->DeleteLocalRef(env, jSender);
(*env)->DeleteLocalRef(env, jPayload);
release_env(did_attach);
}
static void on_event_cb(int32_t event_type, const char* detail_json) {
int did_attach = 0;
JNIEnv* env = get_env(&did_attach);
if (env == NULL || g_listener == NULL) {
return;
}
jstring jDetail = (*env)->NewStringUTF(env, detail_json);
(*env)->CallVoidMethod(env, g_listener, g_onEvent, (jint)event_type, jDetail);
(*env)->DeleteLocalRef(env, jDetail);
release_env(did_attach);
}Example JNI entrypoints:
JNIEXPORT jlong JNICALL
Java_com_example_moss_MossNode_nativeInit(JNIEnv* env, jclass cls, jstring meshId, jstring configJson) {
(void)cls;
const char* mesh = (*env)->GetStringUTFChars(env, meshId, NULL);
const char* cfg = configJson ? (*env)->GetStringUTFChars(env, configJson, NULL) : NULL;
MossHandle handle = Moss_Init(mesh, NULL, cfg);
if (cfg) {
(*env)->ReleaseStringUTFChars(env, configJson, cfg);
}
(*env)->ReleaseStringUTFChars(env, meshId, mesh);
return (jlong)handle;
}
JNIEXPORT void JNICALL
Java_com_example_moss_MossNode_nativeSetListener(JNIEnv* env, jclass cls, jlong handle, jobject listener) {
(void)cls;
if (g_listener) {
(*env)->DeleteGlobalRef(env, g_listener);
g_listener = NULL;
}
g_listener = (*env)->NewGlobalRef(env, listener);
jclass listenerCls = (*env)->GetObjectClass(env, listener);
g_onMessage = (*env)->GetMethodID(env, listenerCls, "onMessage", "(Ljava/lang/String;[B[B)V");
g_onEvent = (*env)->GetMethodID(env, listenerCls, "onEvent", "(ILjava/lang/String;)V");
Moss_SetCallback((MossHandle)handle, on_message_cb);
Moss_SetEventCallback((MossHandle)handle, on_event_cb);
}Critical rules:
- keep the Java listener as a global ref, not a local ref
- delete and replace the old global ref when swapping listeners
- attach native callback threads to the JVM before invoking Java
- detach them afterwards if you attached them
- copy callback payloads into Java-owned arrays before returning
For Android, the JNI pattern is the same, but you also need to think about:
- where you store identity blobs
- background service lifecycle
- foreground service requirements for long-running connectivity
- packaging per ABI
Typical ABI output matrix:
arm64-v8a- optionally
x86_64for emulator builds
If you want stable peer identity across restarts, register Moss_SetKeyStore before Moss_Init.
Recommended behavior:
- host loads previously saved identity blob into a byte buffer
- host returns length from the load callback
- host persists new identity from the save callback
This should live in your app's durable storage layer, not a temp directory.
Moss_SetScoringCallback is optional. Use it only if your host app has a real policy reason to override peer score.
Good use cases:
- prefer known corporate relays
- deprioritize low-trust peers
- integrate host-level health signals
Bad use cases:
- arbitrary score randomization
- blocking inside the scoring callback
- network I/O inside the callback
Most common causes:
- invalid JSON config
- invalid listen port
- identity restore failure through host keystore callbacks
Cause:
- callback invoked on background thread
Fix:
- dispatch callback work to your UI/main thread
Cause:
- forgetting
Moss_FreeforMoss_GetMeshInfo,Moss_GetPublicKey, orMoss_GetNATType
Cause:
- host callback object got garbage-collected or freed
- wrong callback signature
- callback touching UI objects from a non-UI thread
Do not expose raw C pointers throughout your application.
Wrap Moss behind a small host-native API:
create(meshId, config)start()stop()subscribe(channel)unsubscribe(channel)publish(channel, bytes)connect(addr)meshInfoJson()natType()setListener(listener)
Keep the rest of your app unaware of MossHandle, raw buffers, and Moss_Free.
Reference examples in this repository:
- C:
examples/c_example - C++:
examples/cpp_example - C#:
examples/csharp_example - Python:
examples/python_example - Rust:
examples/rust_example
Use those for exact symbol usage. Use this document for production integration structure.