Initial for liblmapi library (static & dynamic) based on current state of lmapi. nix build support added.

This commit is contained in:
NagyZoltanPeter 2026-02-05 01:51:10 +01:00
parent 09034837e6
commit 10367d9c81
No known key found for this signature in database
GPG Key ID: 3E1F97CF4A7B6F42
12 changed files with 620 additions and 11 deletions

View File

@ -433,10 +433,11 @@ docker-liteprotocoltester-push:
################
## C Bindings ##
################
.PHONY: cbindings cwaku_example libwaku
.PHONY: cbindings cwaku_example libwaku liblmapi
STATIC ?= 0
BUILD_COMMAND ?= libwakuDynamic
LIBWAKU_BUILD_COMMAND ?= libwakuDynamic
LMAPI_BUILD_COMMAND ?= liblmapiDynamic
ifeq ($(detected_OS),Windows)
LIB_EXT_DYNAMIC = dll
@ -452,11 +453,15 @@ endif
LIB_EXT := $(LIB_EXT_DYNAMIC)
ifeq ($(STATIC), 1)
LIB_EXT = $(LIB_EXT_STATIC)
BUILD_COMMAND = libwakuStatic
LIBWAKU_BUILD_COMMAND = libwakuStatic
LMAPI_BUILD_COMMAND = liblmapiStatic
endif
libwaku: | build deps librln
echo -e $(BUILD_MSG) "build/$@.$(LIB_EXT)" && $(ENV_SCRIPT) nim $(BUILD_COMMAND) $(NIM_PARAMS) waku.nims $@.$(LIB_EXT)
echo -e $(BUILD_MSG) "build/$@.$(LIB_EXT)" && $(ENV_SCRIPT) nim $(LIBWAKU_BUILD_COMMAND) $(NIM_PARAMS) waku.nims $@.$(LIB_EXT)
liblmapi: | build deps librln
echo -e $(BUILD_MSG) "build/$@.$(LIB_EXT)" && $(ENV_SCRIPT) nim $(LMAPI_BUILD_COMMAND) $(NIM_PARAMS) waku.nims $@.$(LIB_EXT)
#####################
## Mobile Bindings ##

View File

@ -71,6 +71,13 @@
zerokitRln = zerokit.packages.${system}.rln;
};
liblmapi = pkgs.callPackage ./nix/default.nix {
inherit stableSystems;
src = self;
targets = ["liblmapi"];
zerokitRln = zerokit.packages.${system}.rln;
};
default = libwaku;
});

10
liblmapi/declare_lib.nim Normal file
View File

@ -0,0 +1,10 @@
import ffi
import waku/factory/waku
declareLibrary("lmapi")
proc lmapi_set_event_callback(
ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer
) {.dynlib, exportc, cdecl.} =
ctx[].eventCallback = cast[pointer](callback)
ctx[].eventUserData = userData

View File

@ -0,0 +1,173 @@
#include "../liblmapi.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
// Helper function to extract a JSON string field value
// Very basic parser - for production use a proper JSON library
const char* extract_json_field(const char *json, const char *field, char *buffer, size_t bufSize) {
char searchStr[256];
snprintf(searchStr, sizeof(searchStr), "\"%s\":\"", field);
const char *start = strstr(json, searchStr);
if (!start) {
return NULL;
}
start += strlen(searchStr);
const char *end = strchr(start, '"');
if (!end) {
return NULL;
}
size_t len = end - start;
if (len >= bufSize) {
len = bufSize - 1;
}
memcpy(buffer, start, len);
buffer[len] = '\0';
return buffer;
}
// Event callback that handles message events
void event_callback(int ret, const char *msg, size_t len, void *userData) {
if (ret != RET_OK || msg == NULL || len == 0) {
return;
}
// Create null-terminated string for easier parsing
char *eventJson = malloc(len + 1);
if (!eventJson) {
return;
}
memcpy(eventJson, msg, len);
eventJson[len] = '\0';
// Extract eventType
char eventType[64];
if (!extract_json_field(eventJson, "eventType", eventType, sizeof(eventType))) {
free(eventJson);
return;
}
// Handle different event types
if (strcmp(eventType, "message_sent") == 0) {
char requestId[128];
char messageHash[128];
extract_json_field(eventJson, "requestId", requestId, sizeof(requestId));
extract_json_field(eventJson, "messageHash", messageHash, sizeof(messageHash));
printf("📤 [EVENT] Message sent - RequestID: %s, Hash: %s\n", requestId, messageHash);
} else if (strcmp(eventType, "message_error") == 0) {
char requestId[128];
char messageHash[128];
char error[256];
extract_json_field(eventJson, "requestId", requestId, sizeof(requestId));
extract_json_field(eventJson, "messageHash", messageHash, sizeof(messageHash));
extract_json_field(eventJson, "error", error, sizeof(error));
printf("❌ [EVENT] Message error - RequestID: %s, Hash: %s, Error: %s\n",
requestId, messageHash, error);
} else if (strcmp(eventType, "message_propagated") == 0) {
char requestId[128];
char messageHash[128];
extract_json_field(eventJson, "requestId", requestId, sizeof(requestId));
extract_json_field(eventJson, "messageHash", messageHash, sizeof(messageHash));
printf("✅ [EVENT] Message propagated - RequestID: %s, Hash: %s\n", requestId, messageHash);
} else {
printf(" [EVENT] Unknown event type: %s\n", eventType);
}
free(eventJson);
}
// Simple callback that prints results
void simple_callback(int ret, const char *msg, size_t len, void *userData) {
const char *operation = (const char *)userData;
if (ret == RET_OK) {
if (len > 0) {
printf("[%s] Success: %.*s\n", operation, (int)len, msg);
} else {
printf("[%s] Success\n", operation);
}
} else {
printf("[%s] Error: %.*s\n", operation, (int)len, msg);
}
}
int main() {
printf("=== Logos Messaging API (LMAPI) Example ===\n\n");
// Configuration JSON for creating a node
const char *config = "{"
"\"mode\": \"Core\","
"\"clusterId\": 1,"
"\"entryNodes\": [],"
"\"networkingConfig\": {"
"\"listenIpv4\": \"0.0.0.0\","
"\"p2pTcpPort\": 60000,"
"\"discv5UdpPort\": 9000"
"}"
"}";
printf("1. Creating node...\n");
void *ctx = lmapi_create_node(config, simple_callback, (void *)"create_node");
if (ctx == NULL) {
printf("Failed to create node\n");
return 1;
}
// Wait a bit for the callback
sleep(1);
printf("\n2. Setting up event callback...\n");
lmapi_set_event_callback(ctx, event_callback, NULL);
printf("Event callback registered for message events\n");
printf("\n3. Starting node...\n");
lmapi_start_node(ctx, simple_callback, (void *)"start_node");
// Wait for node to start
sleep(2);
printf("\n4. Subscribing to content topic...\n");
const char *contentTopic = "/example/1/chat/proto";
lmapi_subscribe(ctx, simple_callback, (void *)"subscribe", contentTopic);
// Wait for subscription
sleep(1);
printf("\n5. Sending a message...\n");
printf("Watch for message events (sent, propagated, or error):\n");
// Create base64-encoded payload: "Hello, Logos Messaging!"
const char *message = "{"
"\"contentTopic\": \"/example/1/chat/proto\","
"\"payload\": \"SGVsbG8sIExvZ29zIE1lc3NhZ2luZyE=\","
"\"ephemeral\": false"
"}";
lmapi_send(ctx, simple_callback, (void *)"send", message);
// Wait for message events to arrive
printf("Waiting for message delivery events...\n");
sleep(3);
printf("\n6. Unsubscribing from content topic...\n");
lmapi_unsubscribe(ctx, simple_callback, (void *)"unsubscribe", contentTopic);
sleep(1);
printf("\n7. Stopping node...\n");
lmapi_stop_node(ctx, simple_callback, (void *)"stop_node");
sleep(1);
printf("\n8. Destroying context...\n");
lmapi_destroy(ctx, simple_callback, (void *)"destroy");
printf("\n=== Example completed ===\n");
return 0;
}

27
liblmapi/json_event.nim Normal file
View File

@ -0,0 +1,27 @@
import std/[json, macros]
type JsonEvent*[T] = ref object
eventType*: string
payload*: T
macro toFlatJson*(event: JsonEvent): JsonNode =
## Serializes JsonEvent[T] to flat JSON with eventType first,
## followed by all fields from T's payload
result = quote:
var jsonObj = newJObject()
jsonObj["eventType"] = %`event`.eventType
# Serialize payload fields into the same object (flattening)
let payloadJson = %`event`.payload
for key, val in payloadJson.pairs:
jsonObj[key] = val
jsonObj
proc `$`*[T](event: JsonEvent[T]): string =
$toFlatJson(event)
proc newJsonEvent*[T](eventType: string, payload: T): JsonEvent[T] =
## Creates a new JsonEvent with the given eventType and payload.
## The payload's fields will be flattened into the JSON output.
JsonEvent[T](eventType: eventType, payload: payload)

81
liblmapi/liblmapi.h Normal file
View File

@ -0,0 +1,81 @@
// Generated manually and inspired by libwaku.h
// Header file for Logos Messaging API (LMAPI) library
#ifndef __liblmapi__
#define __liblmapi__
#include <stddef.h>
#include <stdint.h>
// The possible returned values for the functions that return int
#define RET_OK 0
#define RET_ERR 1
#define RET_MISSING_CALLBACK 2
#ifdef __cplusplus
extern "C"
{
#endif
typedef void (*FFICallBack)(int callerRet, const char *msg, size_t len, void *userData);
// Creates a new instance of the node from the given configuration JSON.
// Returns a pointer to the Context needed by the rest of the API functions.
// Configuration should be in JSON format following the NodeConfig structure.
void *lmapi_create_node(
const char *configJson,
FFICallBack callback,
void *userData);
// Starts the node.
int lmapi_start_node(void *ctx,
FFICallBack callback,
void *userData);
// Stops the node.
int lmapi_stop_node(void *ctx,
FFICallBack callback,
void *userData);
// Destroys an instance of a node created with lmapi_create_node
int lmapi_destroy(void *ctx,
FFICallBack callback,
void *userData);
// Subscribe to a content topic.
// contentTopic: string representing the content topic (e.g., "/myapp/1/chat/proto")
int lmapi_subscribe(void *ctx,
FFICallBack callback,
void *userData,
const char *contentTopic);
// Unsubscribe from a content topic.
int lmapi_unsubscribe(void *ctx,
FFICallBack callback,
void *userData,
const char *contentTopic);
// Send a message.
// messageJson: JSON string with the following structure:
// {
// "contentTopic": "/myapp/1/chat/proto",
// "payload": "base64-encoded-payload",
// "ephemeral": false
// }
// Returns a request ID that can be used to track the message delivery.
int lmapi_send(void *ctx,
FFICallBack callback,
void *userData,
const char *messageJson);
// Sets a callback that will be invoked whenever an event occurs.
// It is crucial that the passed callback is fast, non-blocking and potentially thread-safe.
void lmapi_set_event_callback(void *ctx,
FFICallBack callback,
void *userData);
#ifdef __cplusplus
}
#endif
#endif /* __liblmapi__ */

29
liblmapi/liblmapi.nim Normal file
View File

@ -0,0 +1,29 @@
import std/[atomics, options]
import chronicles, chronos, chronos/threadsync, ffi
import waku/factory/waku, waku/node/waku_node, ./declare_lib
################################################################################
## Include different APIs, i.e. all procs with {.ffi.} pragma
include ./lmapi/node_api, ./lmapi/messaging_api
################################################################################
### Exported procs
proc lmapi_destroy(
ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer
): cint {.dynlib, exportc, cdecl.} =
initializeLibrary()
checkParams(ctx, callback, userData)
ffi.destroyFFIContext(ctx).isOkOr:
let msg = "liblmapi error: " & $error
callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData)
return RET_ERR
## always need to invoke the callback although we don't retrieve value to the caller
callback(RET_OK, nil, 0, userData)
return RET_OK
# ### End of exported procs
# ################################################################################

View File

@ -0,0 +1,86 @@
import std/[json, base64]
import chronos, results, ffi
import stew/byteutils
import
waku/factory/waku,
waku/waku_core/topics/content_topic,
waku/api/[api, types],
../declare_lib
proc lmapi_subscribe(
ctx: ptr FFIContext[Waku],
callback: FFICallBack,
userData: pointer,
contentTopicStr: cstring,
) {.ffi.} =
# ContentTopic is just a string type alias
let contentTopic = ContentTopic($contentTopicStr)
(await api.subscribe(ctx.myLib[], contentTopic)).isOkOr:
let errMsg = $error
return err("Subscribe failed: " & errMsg)
return ok("")
proc lmapi_unsubscribe(
ctx: ptr FFIContext[Waku],
callback: FFICallBack,
userData: pointer,
contentTopicStr: cstring,
) {.ffi.} =
# ContentTopic is just a string type alias
let contentTopic = ContentTopic($contentTopicStr)
api.unsubscribe(ctx.myLib[], contentTopic).isOkOr:
let errMsg = $error
return err("Unsubscribe failed: " & errMsg)
return ok("")
proc lmapi_send(
ctx: ptr FFIContext[Waku],
callback: FFICallBack,
userData: pointer,
messageJson: cstring,
) {.ffi.} =
## Parse the message JSON and send the message
var jsonNode: JsonNode
try:
jsonNode = parseJson($messageJson)
except Exception as e:
return err("Failed to parse message JSON: " & e.msg)
# Extract content topic
if not jsonNode.hasKey("contentTopic"):
return err("Missing contentTopic field")
# ContentTopic is just a string type alias
let contentTopic = ContentTopic(jsonNode["contentTopic"].getStr())
# Extract payload (expect base64 encoded string)
if not jsonNode.hasKey("payload"):
return err("Missing payload field")
var payload: seq[byte]
try:
let payloadStr = jsonNode["payload"].getStr()
# base64.decode returns string, convert to seq[byte]
let decodedStr = base64.decode(payloadStr)
payload = cast[seq[byte]](decodedStr)
except Exception as e:
return err("Failed to decode payload: " & e.msg)
# Extract ephemeral flag
let ephemeral = jsonNode.getOrDefault("ephemeral").getBool(false)
# Create message envelope
let envelope = MessageEnvelope.init(
contentTopic = contentTopic, payload = payload, ephemeral = ephemeral
)
# Send the message
let requestId = (await api.send(ctx.myLib[], envelope)).valueOr:
let errMsg = $error
return err("Send failed: " & errMsg)
return ok($requestId)

155
liblmapi/lmapi/node_api.nim Normal file
View File

@ -0,0 +1,155 @@
import std/[json, options]
import chronos, results, ffi
import
waku/factory/waku,
waku/node/waku_node,
waku/api/[api, api_conf, types],
waku/events/message_events,
../declare_lib,
../json_event
# Add JSON serialization for RequestId
proc `%`*(id: RequestId): JsonNode =
%($id)
registerReqFFI(CreateNodeRequest, ctx: ptr FFIContext[Waku]):
proc(configJson: cstring): Future[Result[string, string]] {.async.} =
## Parse the JSON configuration and create a node
var jsonNode: JsonNode
try:
jsonNode = parseJson($configJson)
except Exception as e:
return err("Failed to parse config JSON: " & e.msg)
# Extract basic configuration
let mode =
if jsonNode.hasKey("mode") and jsonNode["mode"].getStr() == "Edge":
WakuMode.Edge
else:
WakuMode.Core
# Build protocols config
var entryNodes: seq[string] = @[]
if jsonNode.hasKey("entryNodes"):
for node in jsonNode["entryNodes"]:
entryNodes.add(node.getStr())
var staticStoreNodes: seq[string] = @[]
if jsonNode.hasKey("staticStoreNodes"):
for node in jsonNode["staticStoreNodes"]:
staticStoreNodes.add(node.getStr())
let clusterId =
if jsonNode.hasKey("clusterId"):
uint16(jsonNode["clusterId"].getInt())
else:
1u16 # Default cluster ID
# Build networking config
let networkingConfig =
if jsonNode.hasKey("networkingConfig"):
let netJson = jsonNode["networkingConfig"]
NetworkingConfig(
listenIpv4: netJson.getOrDefault("listenIpv4").getStr("0.0.0.0"),
p2pTcpPort: uint16(netJson.getOrDefault("p2pTcpPort").getInt(60000)),
discv5UdpPort: uint16(netJson.getOrDefault("discv5UdpPort").getInt(9000)),
)
else:
DefaultNetworkingConfig
# Build protocols config
let protocolsConfig = ProtocolsConfig.init(
entryNodes = entryNodes,
staticStoreNodes = staticStoreNodes,
clusterId = clusterId,
)
# Build node config
let nodeConfig = NodeConfig.init(
mode = mode,
protocolsConfig = protocolsConfig,
networkingConfig = networkingConfig,
)
# Create the node
ctx.myLib[] = (await api.createNode(nodeConfig)).valueOr:
let errMsg = $error
chronicles.error "CreateNodeRequest failed", err = errMsg
return err(errMsg)
return ok("")
proc lmapi_create_node(
configJson: cstring, callback: FFICallback, userData: pointer
): pointer {.dynlib, exportc, cdecl.} =
initializeLibrary()
if isNil(callback):
echo "error: missing callback in lmapi_create_node"
return nil
var ctx = ffi.createFFIContext[Waku]().valueOr:
let msg = "Error in createFFIContext: " & $error
callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData)
return nil
ctx.userData = userData
ffi.sendRequestToFFIThread(
ctx, CreateNodeRequest.ffiNewReq(callback, userData, configJson)
).isOkOr:
let msg = "error in sendRequestToFFIThread: " & $error
callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData)
return nil
return ctx
proc lmapi_start_node(
ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer
) {.ffi.} =
# setting up outgoing event listeners
let sentListener = MessageSentEvent.listen(
ctx.myLib[].brokerCtx,
proc(event: MessageSentEvent) {.async: (raises: []).} =
callEventCallback(ctx, "onMessageSent"):
$newJsonEvent("message_sent", event),
).valueOr:
chronicles.error "MessageSentEvent.listen failed", err = $error
return err("MessageSentEvent.listen failed: " & $error)
let errorListener = MessageErrorEvent.listen(
ctx.myLib[].brokerCtx,
proc(event: MessageErrorEvent) {.async: (raises: []).} =
callEventCallback(ctx, "onMessageError"):
$newJsonEvent("message_error", event),
).valueOr:
chronicles.error "MessageErrorEvent.listen failed", err = $error
return err("MessageErrorEvent.listen failed: " & $error)
let propagatedListener = MessagePropagatedEvent.listen(
ctx.myLib[].brokerCtx,
proc(event: MessagePropagatedEvent) {.async: (raises: []).} =
callEventCallback(ctx, "onMessagePropagated"):
$newJsonEvent("message_propagated", event),
).valueOr:
chronicles.error "MessagePropagatedEvent.listen failed", err = $error
return err("MessagePropagatedEvent.listen failed: " & $error)
(await startWaku(addr ctx.myLib[])).isOkOr:
let errMsg = $error
chronicles.error "START_NODE failed", err = errMsg
return err("failed to start: " & errMsg)
return ok("")
proc lmapi_stop_node(
ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer
) {.ffi.} =
MessageErrorEvent.dropAllListeners(ctx.myLib[].brokerCtx)
MessageSentEvent.dropAllListeners(ctx.myLib[].brokerCtx)
MessagePropagatedEvent.dropAllListeners(ctx.myLib[].brokerCtx)
(await ctx.myLib[].stop()).isOkOr:
let errMsg = $error
chronicles.error "STOP_NODE failed", err = errMsg
return err("failed to stop: " & errMsg)
return ok("")

27
liblmapi/nim.cfg Normal file
View File

@ -0,0 +1,27 @@
# Nim configuration for liblmapi
# Ensure correct compiler configuration
--gc:
refc
--threads:
on
# Include paths
--path:
"../vendor/nim-ffi"
--path:
"../"
# Optimization and debugging
--opt:
speed
--debugger:
native
# Export symbols for dynamic library
--app:
lib
--noMain
# Enable FFI macro features when needed for debugging
# --define:ffiDumpMacros

View File

@ -94,8 +94,9 @@ in stdenv.mkDerivation {
# Copy library files
cp build/* $out/bin/ 2>/dev/null || true
# Copy the header file
cp library/libwaku.h $out/include/
# Copy header files
cp library/libwaku.h $out/include/ 2>/dev/null || true
cp liblmapi/liblmapi.h $out/include/ 2>/dev/null || true
'';
meta = with pkgs.lib; {

View File

@ -64,7 +64,7 @@ proc buildBinary(name: string, srcDir = "./", params = "", lang = "c") =
exec "nim " & lang & " --out:build/" & name & " --mm:refc " & extra_params & " " &
srcDir & name & ".nim"
proc buildLibrary(lib_name: string, srcDir = "./", params = "", `type` = "static") =
proc buildLibrary(lib_name: string, srcDir = "./", params = "", `type` = "static", srcFile = "libwaku.nim", mainPrefix = "libwaku") =
if not dirExists "build":
mkDir "build"
# allow something like "nim nimbus --verbosity:0 --hints:off nimbus.nims"
@ -73,12 +73,12 @@ proc buildLibrary(lib_name: string, srcDir = "./", params = "", `type` = "static
extra_params &= " " & paramStr(i)
if `type` == "static":
exec "nim c" & " --out:build/" & lib_name &
" --threads:on --app:staticlib --opt:size --noMain --mm:refc --header -d:metrics --nimMainPrefix:libwaku --skipParentCfg:on -d:discv5_protocol_id=d5waku " &
extra_params & " " & srcDir & "libwaku.nim"
" --threads:on --app:staticlib --opt:size --noMain --mm:refc --header -d:metrics --nimMainPrefix:" & mainPrefix & " --skipParentCfg:on -d:discv5_protocol_id=d5waku " &
extra_params & " " & srcDir & srcFile
else:
exec "nim c" & " --out:build/" & lib_name &
" --threads:on --app:lib --opt:size --noMain --mm:refc --header -d:metrics --nimMainPrefix:libwaku --skipParentCfg:off -d:discv5_protocol_id=d5waku " &
extra_params & " " & srcDir & "libwaku.nim"
" --threads:on --app:lib --opt:size --noMain --mm:refc --header -d:metrics --nimMainPrefix:" & mainPrefix & " --skipParentCfg:off -d:discv5_protocol_id=d5waku " &
extra_params & " " & srcDir & srcFile
proc buildMobileAndroid(srcDir = ".", params = "") =
let cpu = getEnv("CPU")
@ -400,3 +400,11 @@ task libWakuIOS, "Build the mobile bindings for iOS":
let srcDir = "./library"
let extraParams = "-d:chronicles_log_level=ERROR"
buildMobileIOS srcDir, extraParams
task liblmapiStatic, "Build the liblmapi (Logos Messaging API) static library":
let lib_name = paramStr(paramCount())
buildLibrary lib_name, "liblmapi/", chroniclesParams, "static", "liblmapi.nim", "liblmapi"
task liblmapiDynamic, "Build the liblmapi (Logos Messaging API) dynamic library":
let lib_name = paramStr(paramCount())
buildLibrary lib_name, "liblmapi/", chroniclesParams, "dynamic", "liblmapi.nim", "liblmapi"