logos-delivery/messaging/api/ffi/messaging_ffi.nim

375 lines
13 KiB
Nim

## FFI surface for `liblogosdelivery.so`. Exported C functions use the
## `logosdelivery_*` prefix; C declarations live in `liblogosdelivery.h`.
import std/[json, locks, strutils, tables]
import chronos, chronicles, results, ffi
import stew/byteutils
import waku/common/base64
import waku/factory/waku
import waku/factory/waku_state_info
import waku/api/ffi/kernel_helpers
import waku/waku_core/topics/content_topic
import layers/logos_delivery
import messaging/api/types
import messaging/api/events
import messaging/api/messaging as messaging_brokers
import tools/confutils/cli_args
import tools/confutils/config_option_meta
import messaging/api/ffi/json_event
# `RequestId` is rendered via `$`.
proc `%`*(id: RequestId): JsonNode =
%($id)
var eventCallbackLock: Lock
initLock(eventCallbackLock)
# Event listener handles registered at start, kept per broker context so stop
# drops exactly these.
type MessagingFFIListeners = object
sent: MessageSentEventListener
error: MessageErrorEventListener
propagated: MessagePropagatedEventListener
received: MessageReceivedEventListener
connStatus: EventConnectionStatusChangeListener
var ffiListeners {.threadvar.}: Table[uint32, MessagingFFIListeners]
template requireInitializedMessaging(
ctx: ptr FFIContext[LogosDelivery], opName: string, onError: untyped
) =
if isNil(ctx):
let errMsg {.inject.} = opName & " failed: invalid context"
onError
elif isNil(ctx.myLib) or isNil(ctx.myLib[]):
let errMsg {.inject.} = opName & " failed: client is not initialized"
onError
# ---- Construction requests (run on the FFI worker thread) ----
registerReqFFI(CreateMessagingClientByPresetMode, ctx: ptr FFIContext[LogosDelivery]):
proc(preset: cstring, mode: cstring): Future[Result[string, string]] {.async.} =
let modeEnum =
try:
parseEnum[WakuMode]($mode)
except ValueError:
return err("Invalid mode value: " & $mode)
ctx.myLib[] = (await LogosDelivery.new(MessagingClient, $preset, modeEnum)).valueOr:
chronicles.error "CreateMessagingClientByPresetMode failed", err = error
return err(error)
return ok("")
registerReqFFI(CreateMessagingClientByConf, ctx: ptr FFIContext[LogosDelivery]):
proc(configJson: cstring): Future[Result[string, string]] {.async.} =
let waku = (await createWakuFromJson(configJson)).valueOr:
chronicles.error "CreateMessagingClientByConf: createWakuFromJson failed",
err = error
return err(error)
ctx.myLib[] = LogosDelivery.new(MessagingClient, waku).valueOr:
chronicles.error "CreateMessagingClientByConf: LogosDelivery.new failed",
err = error
return err(error)
return ok("")
# ---- C exports ----
proc logosdelivery_create_node_preset_mode(
preset: cstring, mode: cstring, callback: FFICallback, userData: pointer
): pointer {.dynlib, exportc, cdecl.} =
## Create a node from a preset name and mode string.
initializeLibrary()
if isNil(callback):
echo "error: missing callback in logosdelivery_create_node_preset_mode"
return nil
var ctx = ffi.createFFIContext[LogosDelivery]().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,
CreateMessagingClientByPresetMode.ffiNewReq(callback, userData, preset, mode),
).isOkOr:
let msg = "error in sendRequestToFFIThread: " & $error
callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData)
ffi.destroyFFIContext(ctx).isOkOr:
chronicles.error "destroyFFIContext failed after sendRequestToFFIThread error",
err = $error
return nil
return ctx
proc logosdelivery_create_node(
configJson: cstring, callback: FFICallback, userData: pointer
): pointer {.dynlib, exportc, cdecl.} =
initializeLibrary()
if isNil(callback):
echo "error: missing callback in logosdelivery_create_node"
return nil
var ctx = ffi.createFFIContext[LogosDelivery]().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, CreateMessagingClientByConf.ffiNewReq(callback, userData, configJson)
).isOkOr:
let msg = "error in sendRequestToFFIThread: " & $error
callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData)
ffi.destroyFFIContext(ctx).isOkOr:
chronicles.error "destroyFFIContext failed after sendRequestToFFIThread error",
err = $error
return nil
return ctx
proc logosdelivery_destroy(
ctx: ptr FFIContext[LogosDelivery], callback: FFICallBack, userData: pointer
): cint {.dynlib, exportc, cdecl.} =
initializeLibrary()
checkParams(ctx, callback, userData)
ffi.destroyFFIContext(ctx).isOkOr:
let msg = "logosdelivery_destroy error: " & $error
callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData)
return RET_ERR
callback(RET_OK, nil, 0, userData)
return RET_OK
proc logosdelivery_set_event_callback(
ctx: ptr FFIContext[LogosDelivery], callback: FFICallBack, userData: pointer
) {.dynlib, exportc, cdecl.} =
if isNil(ctx):
echo "error: invalid context in logosdelivery_set_event_callback"
return
eventCallbackLock.acquire()
defer:
eventCallbackLock.release()
ctx[].eventCallback = cast[pointer](callback)
ctx[].eventUserData = userData
# ---- Lifecycle: start (register event listeners + MessagingClient.start) ----
proc logosdelivery_start_node(
ctx: ptr FFIContext[LogosDelivery], callback: FFICallBack, userData: pointer
) {.ffi.} =
requireInitializedMessaging(ctx, "logosdelivery_start_node"):
return err(errMsg)
let brokerCtx = ctx.myLib[].brokerCtx
let sentListener = MessageSentEvent.listen(
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(
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(
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)
let receivedListener = MessageReceivedEvent.listen(
brokerCtx,
proc(event: MessageReceivedEvent) {.async: (raises: []).} =
callEventCallback(ctx, "onMessageReceived"):
$newJsonEvent("message_received", event),
).valueOr:
chronicles.error "MessageReceivedEvent.listen failed", err = $error
return err("MessageReceivedEvent.listen failed: " & $error)
let connStatusListener = EventConnectionStatusChange.listen(
brokerCtx,
proc(event: EventConnectionStatusChange) {.async: (raises: []).} =
callEventCallback(ctx, "onConnectionStatusChange"):
$newJsonEvent("connection_status_change", event),
).valueOr:
chronicles.error "EventConnectionStatusChange.listen failed", err = $error
return err("EventConnectionStatusChange.listen failed: " & $error)
ffiListeners[brokerCtx.uint32] = MessagingFFIListeners(
sent: sentListener,
error: errorListener,
propagated: propagatedListener,
received: receivedListener,
connStatus: connStatusListener,
)
(await ctx.myLib[].start()).isOkOr:
chronicles.error "logosdelivery_start_node failed", err = error
return err("failed to start: " & error)
return ok("")
# ---- Lifecycle: stop (drop listeners + MessagingClient.stop) ----
proc logosdelivery_stop_node(
ctx: ptr FFIContext[LogosDelivery], callback: FFICallBack, userData: pointer
) {.ffi.} =
requireInitializedMessaging(ctx, "logosdelivery_stop_node"):
return err(errMsg)
let brokerCtx = ctx.myLib[].brokerCtx
var listeners: MessagingFFIListeners
if ffiListeners.pop(brokerCtx.uint32, listeners):
await MessageSentEvent.dropListener(brokerCtx, listeners.sent)
await MessageErrorEvent.dropListener(brokerCtx, listeners.error)
await MessagePropagatedEvent.dropListener(brokerCtx, listeners.propagated)
await MessageReceivedEvent.dropListener(brokerCtx, listeners.received)
await EventConnectionStatusChange.dropListener(brokerCtx, listeners.connStatus)
(await ctx.myLib[].stop()).isOkOr:
chronicles.error "logosdelivery_stop_node failed", err = error
return err("failed to stop: " & error)
return ok("")
# ---- Messaging operations ----
proc logosdelivery_subscribe(
ctx: ptr FFIContext[LogosDelivery],
callback: FFICallBack,
userData: pointer,
contentTopicStr: cstring,
) {.ffi.} =
requireInitializedMessaging(ctx, "logosdelivery_subscribe"):
return err(errMsg)
let contentTopic = ContentTopic($contentTopicStr)
(
await messaging_brokers.RequestMessagingSubscribe.request(
ctx.myLib[].brokerCtx, contentTopic
)
).isOkOr:
return err("subscribe failed: " & error)
return ok("")
proc logosdelivery_unsubscribe(
ctx: ptr FFIContext[LogosDelivery],
callback: FFICallBack,
userData: pointer,
contentTopicStr: cstring,
) {.ffi.} =
requireInitializedMessaging(ctx, "logosdelivery_unsubscribe"):
return err(errMsg)
let contentTopic = ContentTopic($contentTopicStr)
messaging_brokers.RequestMessagingUnsubscribe.request(
ctx.myLib[].brokerCtx, contentTopic
).isOkOr:
return err("unsubscribe failed: " & error)
return ok("")
proc logosdelivery_send(
ctx: ptr FFIContext[LogosDelivery],
callback: FFICallBack,
userData: pointer,
messageJson: cstring,
) {.ffi.} =
requireInitializedMessaging(ctx, "logosdelivery_send"):
return err(errMsg)
var jsonNode: JsonNode
try:
jsonNode = parseJson($messageJson)
except Exception as e:
return err("Failed to parse message JSON: " & e.msg)
if not jsonNode.hasKey("contentTopic"):
return err("Missing contentTopic field")
let contentTopic = ContentTopic(jsonNode["contentTopic"].getStr())
if not jsonNode.hasKey("payload"):
return err("Missing payload field")
let payloadStr = jsonNode["payload"].getStr()
let payload = base64.decode(Base64String(payloadStr)).valueOr:
return err("invalid payload format: " & error)
let ephemeral = jsonNode.getOrDefault("ephemeral").getBool(false)
let envelope = MessageEnvelope.init(
contentTopic = contentTopic, payload = payload, ephemeral = ephemeral
)
let sendResp = (
await messaging_brokers.RequestMessagingSend.request(
ctx.myLib[].brokerCtx, envelope
)
).valueOr:
return err("send failed: " & error)
return ok($sendResp.requestId)
# ---- Debug / introspection ----
proc logosdelivery_get_available_node_info_ids(
ctx: ptr FFIContext[LogosDelivery], callback: FFICallBack, userData: pointer
) {.ffi.} =
## List of all available node info item ids queryable via get_node_info.
requireInitializedMessaging(ctx, "logosdelivery_get_available_node_info_ids"):
return err(errMsg)
return ok($ctx.myLib[].waku.stateInfo.getAllPossibleInfoItemIds())
proc logosdelivery_get_node_info(
ctx: ptr FFIContext[LogosDelivery],
callback: FFICallBack,
userData: pointer,
nodeInfoId: cstring,
) {.ffi.} =
## Content of the node info item with the given id, if it exists.
requireInitializedMessaging(ctx, "logosdelivery_get_node_info"):
return err(errMsg)
let infoItemIdEnum =
try:
parseEnum[NodeInfoId]($nodeInfoId)
except ValueError:
return err("Invalid node info id: " & $nodeInfoId)
return ok(ctx.myLib[].waku.stateInfo.getNodeInfoItem(infoItemIdEnum))
proc logosdelivery_get_available_configs(
ctx: ptr FFIContext[LogosDelivery], callback: FFICallBack, userData: pointer
) {.ffi.} =
## Information about the accepted config items.
requireInitializedMessaging(ctx, "logosdelivery_get_available_configs"):
return err(errMsg)
let optionMetas: seq[ConfigOptionMeta] = extractConfigOptionMeta(WakuNodeConf)
var configOptionDetails = newJArray()
for meta in optionMetas:
configOptionDetails.add(
%*{
meta.fieldName: meta.typeName & "(" & meta.defaultValue & ")", "desc": meta.desc
}
)
var jsonNode = newJObject()
jsonNode["configOptions"] = configOptionDetails
return ok(pretty(jsonNode))