adding setup code similar tu libwaku's

This commit is contained in:
Gabriel mermelstein 2025-04-08 19:28:26 +03:00
parent 07329d70da
commit 8806391371
No known key found for this signature in database
GPG Key ID: 82B8134785FEAE0D

View File

@ -1,363 +1,99 @@
{.pragma: exported, exportc, cdecl, raises: [].}
{.pragma: callback, cdecl, raises: [], gcsafe.}
{.passc: "-fPIC".}
when defined(linux):
{.passl: "-Wl,-soname,libsds.so".}
import std/[locks, typetraits, tables] # Added tables
import chronos
import results
import ../src/[reliability, reliability_utils, message]
type CReliabilityManagerHandle* = pointer
################################################################################
### Wrapper around the reliability manager
################################################################################
type
# Callback Types (Imported from C Header)
CEventType* {.importc: "CEventType", header: "libsds.h", pure.} = enum
EVENT_MESSAGE_READY = 1
EVENT_MESSAGE_SENT = 2
EVENT_MISSING_DEPENDENCIES = 3
EVENT_PERIODIC_SYNC = 4
################################################################################
### Not-exported components
CEventCallback* = proc(
handle: pointer,
eventType: CEventType,
data1: pointer,
data2: pointer,
data3: csize_t,
) {.cdecl.} # Use csize_t
CResult* {.importc: "CResult", header: "libsds.h", bycopy.} = object
is_ok*: bool
error_message*: cstring
CWrapResult* {.importc: "CWrapResult", header: "libsds.h", bycopy.} = object
base_result*: CResult
message*: pointer
message_len*: csize_t
CUnwrapResult* {.importc: "CUnwrapResult", header: "libsds.h", bycopy.} = object
base_result*: CResult
message*: pointer
message_len*: csize_t
missing_deps*: ptr cstring
missing_deps_count*: csize_t
# --- Callback Registry ---
type CallbackRegistry = Table[CReliabilityManagerHandle, CEventCallback]
var
callbackRegistry: CallbackRegistry
registryLock: Lock
initLock(registryLock)
# --- Memory Management Helpers ---
proc allocCString*(s: string): cstring {.inline, gcsafe.} =
if s.len == 0:
return nil
result = cast[cstring](allocShared(s.len + 1))
copyMem(result, s.cstring, s.len + 1)
proc allocSeqByte*(s: seq[byte]): (pointer, csize_t) {.inline, gcsafe.} =
if s.len == 0:
return (nil, 0)
let len = s.len
let bufferPtr = allocShared(len)
if len > 0:
copyMem(bufferPtr, cast[pointer](s[0].unsafeAddr), len.Natural)
return (bufferPtr, len.csize_t)
proc allocSeqCString*(
s: seq[string]
): (ptr cstring, csize_t) {.inline, gcsafe, cdecl.} =
if s.len == 0:
return (nil, 0)
let count = s.len
# Allocate memory for 'count' cstring pointers, cast to ptr UncheckedArray
let arrPtr = cast[ptr UncheckedArray[cstring]](allocShared(count * sizeof(cstring)))
for i in 0 ..< count:
# Allocate each string and store its pointer in the array using unchecked array indexing
arrPtr[i] = allocCString(s[i])
# Return pointer to the first element, cast back to ptr cstring
return (cast[ptr cstring](arrPtr), count.csize_t)
proc freeCString*(cs: cstring) {.inline, gcsafe.} =
if cs != nil:
deallocShared(cs)
proc freeSeqByte*(bufferPtr: pointer) {.inline, gcsafe, cdecl.} =
if bufferPtr != nil:
deallocShared(bufferPtr)
# Corrected to accept ptr cstring
proc freeSeqCString*(arrPtr: ptr cstring, count: csize_t) {.inline, gcsafe, cdecl.} =
if arrPtr != nil:
# Cast to ptr UncheckedArray for proper iteration/indexing before freeing
let arr = cast[ptr UncheckedArray[cstring]](arrPtr)
for i in 0 ..< count:
freeCString(arr[i]) # Free each individual cstring
deallocShared(arrPtr) # Free the array pointer itself
# --- Result Conversion Helpers ---
proc toCResultOk*(): CResult =
CResult(is_ok: true, error_message: nil)
proc toCResultErr*(err: ReliabilityError): CResult =
CResult(is_ok: false, error_message: allocCString($err))
proc toCResultErrStr*(errMsg: string): CResult =
CResult(is_ok: false, error_message: allocCString(errMsg))
# --- Callback Wrappers (Nim -> C) ---
# These wrappers call the single global Go callback relay.
proc nimMessageReadyCallback(rm: ReliabilityManager, messageId: MessageID) =
echo "[Nim Binding] nimMessageReadyCallback called for: ", messageId
let handle = cast[CReliabilityManagerHandle](rm)
var cb: CEventCallback
withLock registryLock:
if not callbackRegistry.hasKey(handle):
echo "[Nim Binding] No callback registered for handle: ", cast[int](handle)
return
cb = callbackRegistry[handle]
# Pass handle, event type, and messageId (as data1)
cb(handle, EVENT_MESSAGE_READY, cast[pointer](messageId.cstring), nil, 0)
proc nimMessageSentCallback(rm: ReliabilityManager, messageId: MessageID) =
echo "[Nim Binding] nimMessageSentCallback called for: ", messageId
let handle = cast[CReliabilityManagerHandle](rm)
var cb: CEventCallback
withLock registryLock:
if not callbackRegistry.hasKey(handle):
echo "[Nim Binding] No callback registered for handle: ", cast[int](handle)
return
cb = callbackRegistry[handle]
cb(handle, EVENT_MESSAGE_SENT, cast[pointer](messageId.cstring), nil, 0)
proc nimMissingDependenciesCallback(
rm: ReliabilityManager, messageId: MessageID, missingDeps: seq[MessageID]
template checkLibsdsParams*(
ctx: ptr SdsContext, callback: SdsCallBack, userData: pointer
) =
echo "[Nim Binding] nimMissingDependenciesCallback called for: ",
messageId, " with deps: ", $missingDeps
let handle = cast[CReliabilityManagerHandle](rm)
var cb: CEventCallback
withLock registryLock:
if not callbackRegistry.hasKey(handle):
echo "[Nim Binding] No callback registered for handle: ", cast[int](handle)
return
cb = callbackRegistry[handle]
ctx[].userData = userData
# Prepare data for the callback
var cDepsPtr: ptr cstring = nil
var cDepsCount: csize_t = 0
var cDepsNim: seq[cstring] = @[] # Keep Nim seq alive during call
if missingDeps.len > 0:
cDepsNim = newSeq[cstring](missingDeps.len)
for i, dep in missingDeps:
cDepsNim[i] = dep.cstring # Nim GC manages these cstrings via the seq
cDepsPtr = cast[ptr cstring](cDepsNim[0].addr)
cDepsCount = missingDeps.len.csize_t
if isNil(callback):
return RET_MISSING_CALLBACK
cb(
handle,
EVENT_MISSING_DEPENDENCIES,
cast[pointer](messageId.cstring),
cast[pointer](cDepsPtr),
cDepsCount,
)
proc nimPeriodicSyncCallback(rm: ReliabilityManager) =
echo "[Nim Binding] nimPeriodicSyncCallback called"
let handle = cast[CReliabilityManagerHandle](rm)
var cb: CEventCallback
withLock registryLock:
if not callbackRegistry.hasKey(handle):
echo "[Nim Binding] No callback registered for handle: ", cast[int](handle)
return
cb = callbackRegistry[handle]
cb(handle, EVENT_PERIODIC_SYNC, nil, nil, 0)
# --- Exported C Functions - Using Opaque Pointer ---
proc NewReliabilityManager*(
channelIdCStr: cstring
): CReliabilityManagerHandle {.exportc, dynlib, cdecl, gcsafe.} =
let channelId = $channelIdCStr
if channelId.len == 0:
echo "Error creating ReliabilityManager: Channel ID cannot be empty"
return nil # Return nil pointer
let rmResult = newReliabilityManager(channelId)
if rmResult.isOk:
let rm = rmResult.get()
# Assign anonymous procs that capture 'rm' and call the wrappers
# Ensure signatures match the non-gcsafe fields in ReliabilityManager
rm.onMessageReady = proc(msgId: MessageID) =
nimMessageReadyCallback(rm, msgId)
rm.onMessageSent = proc(msgId: MessageID) =
nimMessageSentCallback(rm, msgId)
rm.onMissingDependencies = proc(msgId: MessageID, deps: seq[MessageID]) =
nimMissingDependenciesCallback(rm, msgId, deps)
rm.onPeriodicSync = proc() =
nimPeriodicSyncCallback(rm)
# Return the Nim ref object cast to the opaque pointer type
let handle = cast[CReliabilityManagerHandle](rm)
GC_ref(rm) # Prevent GC from moving the object while Go holds the handle
return handle
else:
echo "Error creating ReliabilityManager: ", rmResult.error
return nil # Return nil pointer
proc CleanupReliabilityManager*(
handle: CReliabilityManagerHandle
) {.exportc, dynlib, cdecl.} =
let handlePtr = handle
if handlePtr != nil:
# Go side should handle removing the handle from its registry.
# We just need to unref the Nim object.
# No need to interact with gEventCallback here.
# Cast opaque pointer back to Nim ref type
let rm = cast[ReliabilityManager](handlePtr)
cleanup(rm) # Call Nim cleanup
GC_unref(rm) # Allow GC to collect the object now that Go is done
else:
echo "Warning: CleanupReliabilityManager called with NULL handle"
proc ResetReliabilityManager*(
handle: CReliabilityManagerHandle
): CResult {.exportc, dynlib, cdecl, gcsafe.} =
if handle == nil:
return toCResultErrStr("ReliabilityManager handle is NULL")
let rm = cast[ReliabilityManager](handle)
let result = resetReliabilityManager(rm)
if result.isOk:
return toCResultOk()
else:
return toCResultErr(result.error)
proc WrapOutgoingMessage*(
handle: CReliabilityManagerHandle,
messageC: pointer,
messageLen: csize_t,
messageIdCStr: cstring,
): CWrapResult {.exportc, dynlib, cdecl.} = # Keep non-gcsafe
if handle == nil:
template callEventCallback(ctx: ptr SdsContext, eventName: string, body: untyped) =
if isNil(ctx[].eventCallback):
error eventName & " - eventCallback is nil"
return
CWrapResult(base_result: toCResultErrStr("ReliabilityManager handle is NULL"))
let rm = cast[ReliabilityManager](handle)
if messageC == nil and messageLen > 0:
return CWrapResult(
base_result: toCResultErrStr("Message pointer is NULL but length > 0")
)
if messageIdCStr == nil:
return CWrapResult(base_result: toCResultErrStr("Message ID pointer is NULL"))
let messageId = $messageIdCStr
var messageNim: seq[byte]
if messageLen > 0:
messageNim = newSeq[byte](messageLen)
copyMem(messageNim[0].addr, messageC, messageLen.Natural)
else:
messageNim = @[]
let wrapResult = wrapOutgoingMessage(rm, messageNim, messageId)
if wrapResult.isOk:
let (wrappedDataPtr, wrappedDataLen) = allocSeqByte(wrapResult.get())
return CWrapResult(
base_result: toCResultOk(), message: wrappedDataPtr, message_len: wrappedDataLen
)
else:
return CWrapResult(base_result: toCResultErr(wrapResult.error))
proc UnwrapReceivedMessage*(
handle: CReliabilityManagerHandle, messageC: pointer, messageLen: csize_t
): CUnwrapResult {.exportc, dynlib, cdecl.} = # Keep non-gcsafe
if handle == nil:
if isNil(ctx[].eventUserData):
error eventName & " - eventUserData is nil"
return
CUnwrapResult(base_result: toCResultErrStr("ReliabilityManager handle is NULL"))
let rm = cast[ReliabilityManager](handle)
if messageC == nil and messageLen > 0:
return CUnwrapResult(
base_result: toCResultErrStr("Message pointer is NULL but length > 0")
)
foreignThreadGc:
try:
let event = body
cast[SdsCallBack](ctx[].eventCallback)(
RET_OK, unsafeAddr event[0], cast[csize_t](len(event)), ctx[].eventUserData
)
except Exception, CatchableError:
let msg =
"Exception " & eventName & " when calling 'eventCallBack': " &
getCurrentExceptionMsg()
cast[SdsCallBack](ctx[].eventCallback)(
RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), ctx[].eventUserData
)
var messageNim: seq[byte]
if messageLen > 0:
messageNim = newSeq[byte](messageLen)
copyMem(messageNim[0].addr, messageC, messageLen.Natural)
else:
messageNim = @[]
proc handleRequest(
ctx: ptr SdsContext,
requestType: RequestType,
content: pointer,
callback: SdsCallBack,
userData: pointer,
): cint =
sds_thread.sendRequestToSdsThread(ctx, requestType, content, callback, userData).isOkOr:
let msg = "libsds error: " & $error
callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData)
return RET_ERR
let unwrapResult = unwrapReceivedMessage(rm, messageNim)
if unwrapResult.isOk:
let (unwrappedContent, missingDepsNim) = unwrapResult.get()
let (contentPtr, contentLen) = allocSeqByte(unwrappedContent)
let (depsPtr, depsCount) = allocSeqCString(missingDepsNim)
return CUnwrapResult(
base_result: toCResultOk(),
message: contentPtr,
message_len: contentLen,
missing_deps: depsPtr,
missing_deps_count: depsCount,
)
else:
return CUnwrapResult(base_result: toCResultErr(unwrapResult.error))
return RET_OK
proc MarkDependenciesMet*(
handle: CReliabilityManagerHandle, messageIDsC: ptr cstring, count: csize_t
): CResult {.exportc, dynlib, cdecl.} = # Keep non-gcsafe
if handle == nil:
return toCResultErrStr("ReliabilityManager handle is NULL")
let rm = cast[ReliabilityManager](handle)
### End of not-exported components
################################################################################
if messageIDsC == nil and count > 0:
return toCResultErrStr("MessageIDs pointer is NULL but count > 0")
################################################################################
### Library setup
var messageIDsNim = newSeq[string](count)
# Cast to ptr UncheckedArray for indexing
let messageIDsCArray = cast[ptr UncheckedArray[cstring]](messageIDsC)
for i in 0 ..< count:
let currentCStr = messageIDsCArray[i] # Use unchecked array indexing
if currentCStr != nil:
messageIDsNim[i] = $currentCStr
else:
return toCResultErrStr("NULL message ID found in array")
# Every Nim library must have this function called - the name is derived from
# the `--nimMainPrefix` command line option
proc libsdsNimMain() {.importc.}
let result = markDependenciesMet(rm, messageIDsNim)
if result.isOk:
return toCResultOk()
else:
return toCResultErr(result.error)
# To control when the library has been initialized
var initialized: Atomic[bool]
proc RegisterCallback*(
handle: CReliabilityManagerHandle,
cEventCallback: CEventCallback,
cUserDataPtr: pointer,
) {.exportc, dynlib, cdecl.} =
withLock registryLock:
callbackRegistry[handle] = cEventCallback
echo "[Nim Binding] Registered callback for handle: ", cast[int](handle)
if defined(android):
# Redirect chronicles to Android System logs
when compiles(defaultChroniclesStream.outputs[0].writer):
defaultChroniclesStream.outputs[0].writer = proc(
logLevel: LogLevel, msg: LogOutputStr
) {.raises: [].} =
echo logLevel, msg
proc StartPeriodicTasks*(handle: CReliabilityManagerHandle) {.exportc, dynlib, cdecl.} =
if handle == nil:
echo "Error: Cannot start periodic tasks: NULL ReliabilityManager handle"
return
let rm = cast[ReliabilityManager](handle)
startPeriodicTasks(rm)
proc initializeLibrary() {.exported.} =
if not initialized.exchange(true):
## Every Nim library needs to call `<yourprefix>NimMain` once exactly, to initialize the Nim runtime.
## Being `<yourprefix>` the value given in the optional compilation flag --nimMainPrefix:yourprefix
libsdsNimMain()
when declared(setupForeignThreadGc):
setupForeignThreadGc()
when declared(nimGC_setStackBottom):
var locals {.volatile, noinit.}: pointer
locals = addr(locals)
nimGC_setStackBottom(locals)
# --- Memory Freeing Functions ---
proc FreeCResultError*(result: CResult) {.exportc, dynlib, gcsafe, cdecl.} =
freeCString(result.error_message)
proc FreeCWrapResult*(result: CWrapResult) {.exportc, dynlib, gcsafe, cdecl.} =
freeCString(result.base_result.error_message)
freeSeqByte(result.message)
proc FreeCUnwrapResult*(result: CUnwrapResult) {.exportc, dynlib, gcsafe, cdecl.} =
freeCString(result.base_result.error_message)
freeSeqByte(result.message)
freeSeqCString(result.missing_deps, result.missing_deps_count)
### End of library setup
################################################################################