2026-01-29 01:38:08 +07:00
|
|
|
# Nim FFI bindings for libchat conversations library
|
|
|
|
|
|
|
|
|
|
import std/[os]
|
|
|
|
|
|
|
|
|
|
# Dynamic library path resolution
|
|
|
|
|
# Can be overridden at compile time with -d:CONVERSATIONS_LIB:"path/to/lib"
|
|
|
|
|
# Or at runtime via LIBCHAT_LIB environment variable
|
|
|
|
|
when defined(macosx):
|
|
|
|
|
const DEFAULT_LIB_NAME = "liblogos_chat.dylib"
|
|
|
|
|
elif defined(linux):
|
|
|
|
|
const DEFAULT_LIB_NAME = "liblogos_chat.so"
|
|
|
|
|
elif defined(windows):
|
|
|
|
|
const DEFAULT_LIB_NAME = "logos_chat.dll"
|
|
|
|
|
else:
|
|
|
|
|
const DEFAULT_LIB_NAME = "logos_chat"
|
|
|
|
|
|
|
|
|
|
# Try to find the library relative to the source file location at compile time
|
|
|
|
|
const
|
|
|
|
|
thisDir = currentSourcePath().parentDir()
|
|
|
|
|
projectRoot = thisDir.parentDir().parentDir()
|
|
|
|
|
releaseLibPath = projectRoot / "target" / "release" / DEFAULT_LIB_NAME
|
|
|
|
|
debugLibPath = projectRoot / "target" / "debug" / DEFAULT_LIB_NAME
|
|
|
|
|
|
|
|
|
|
# Default to release path, can be overridden with -d:CONVERSATIONS_LIB:"..."
|
|
|
|
|
const CONVERSATIONS_LIB* {.strdefine.} = releaseLibPath
|
|
|
|
|
|
|
|
|
|
# Error codes (must match Rust ErrorCode enum)
|
|
|
|
|
const
|
|
|
|
|
ErrNone* = 0'i32
|
|
|
|
|
ErrBadPtr* = -1'i32
|
|
|
|
|
ErrBadConvoId* = -2'i32
|
|
|
|
|
ErrBadIntro* = -3'i32
|
|
|
|
|
ErrNotImplemented* = -4'i32
|
|
|
|
|
ErrBufferExceeded* = -5'i32
|
|
|
|
|
ErrUnknownError* = -6'i32
|
|
|
|
|
|
|
|
|
|
# Opaque handle type for Context
|
|
|
|
|
type ContextHandle* = pointer
|
|
|
|
|
|
|
|
|
|
type
|
|
|
|
|
## Slice for passing byte arrays to safer_ffi functions
|
|
|
|
|
SliceUint8* = object
|
|
|
|
|
`ptr`*: ptr uint8
|
|
|
|
|
len*: csize_t
|
|
|
|
|
|
|
|
|
|
## Vector type returned by safer_ffi functions (must be freed)
|
|
|
|
|
VecUint8* = object
|
|
|
|
|
`ptr`*: ptr uint8
|
|
|
|
|
len*: csize_t
|
|
|
|
|
cap*: csize_t
|
|
|
|
|
|
|
|
|
|
## repr_c::String type from safer_ffi
|
|
|
|
|
ReprCString* = object
|
|
|
|
|
`ptr`*: ptr char
|
|
|
|
|
len*: csize_t
|
|
|
|
|
cap*: csize_t
|
|
|
|
|
|
|
|
|
|
## Payload structure for FFI (matches Rust Payload struct)
|
|
|
|
|
Payload* = object
|
|
|
|
|
address*: ReprCString
|
|
|
|
|
data*: VecUint8
|
|
|
|
|
|
|
|
|
|
## Vector of Payloads returned by safer_ffi functions
|
|
|
|
|
VecPayload* = object
|
|
|
|
|
`ptr`*: ptr Payload
|
|
|
|
|
len*: csize_t
|
|
|
|
|
cap*: csize_t
|
|
|
|
|
|
|
|
|
|
## Result structure for create_intro_bundle
|
|
|
|
|
## error_code is 0 on success, negative on error (see ErrorCode)
|
2026-02-06 13:41:20 -08:00
|
|
|
CreateIntroResult* = object
|
|
|
|
|
error_code*: int32
|
|
|
|
|
intro_bytes*: VecUint8
|
|
|
|
|
|
|
|
|
|
## Result structure for send_content
|
|
|
|
|
## error_code is 0 on success, negative on error (see ErrorCode)
|
|
|
|
|
SendContentResult* = object
|
2026-01-29 01:38:08 +07:00
|
|
|
error_code*: int32
|
|
|
|
|
payloads*: VecPayload
|
|
|
|
|
|
2026-02-06 13:41:20 -08:00
|
|
|
## Result structure for handle_payload
|
|
|
|
|
## error_code is 0 on success, negative on error (see ErrorCode)
|
|
|
|
|
HandlePayloadResult* = object
|
|
|
|
|
error_code*: int32
|
|
|
|
|
convo_id*: ReprCString
|
|
|
|
|
content*: VecUint8
|
|
|
|
|
|
2026-01-29 01:38:08 +07:00
|
|
|
## Result from create_new_private_convo
|
|
|
|
|
## error_code is 0 on success, negative on error (see ErrorCode)
|
|
|
|
|
NewConvoResult* = object
|
|
|
|
|
error_code*: int32
|
2026-02-06 23:58:23 +07:00
|
|
|
convo_id*: ReprCString
|
2026-01-29 01:38:08 +07:00
|
|
|
payloads*: VecPayload
|
|
|
|
|
|
|
|
|
|
# FFI function imports
|
|
|
|
|
|
|
|
|
|
## Creates a new libchat Context
|
|
|
|
|
## Returns: Opaque handle to the context. Must be freed with destroy_context()
|
|
|
|
|
proc create_context*(): ContextHandle {.importc, dynlib: CONVERSATIONS_LIB.}
|
|
|
|
|
|
|
|
|
|
## Destroys a context and frees its memory
|
|
|
|
|
## - handle must be a valid pointer from create_context()
|
|
|
|
|
## - handle must not be used after this call
|
|
|
|
|
proc destroy_context*(ctx: ContextHandle) {.importc, dynlib: CONVERSATIONS_LIB.}
|
|
|
|
|
|
|
|
|
|
## Creates an intro bundle for sharing with other users
|
2026-02-06 13:41:20 -08:00
|
|
|
## Returns: CreateIntroResult struct - check error_code field (0 = success, negative = error)
|
|
|
|
|
## The result must be freed with destroy_intro_result()
|
2026-01-29 01:38:08 +07:00
|
|
|
proc create_intro_bundle*(
|
|
|
|
|
ctx: ContextHandle,
|
2026-02-06 13:41:20 -08:00
|
|
|
): CreateIntroResult {.importc, dynlib: CONVERSATIONS_LIB.}
|
2026-01-29 01:38:08 +07:00
|
|
|
|
|
|
|
|
## Creates a new private conversation
|
|
|
|
|
## Returns: NewConvoResult struct - check error_code field (0 = success, negative = error)
|
|
|
|
|
## The result must be freed with destroy_convo_result()
|
|
|
|
|
proc create_new_private_convo*(
|
|
|
|
|
ctx: ContextHandle,
|
|
|
|
|
bundle: SliceUint8,
|
|
|
|
|
content: SliceUint8,
|
|
|
|
|
): NewConvoResult {.importc, dynlib: CONVERSATIONS_LIB.}
|
|
|
|
|
|
2026-01-29 01:50:41 +07:00
|
|
|
## Sends content to an existing conversation
|
2026-02-06 13:41:20 -08:00
|
|
|
## Returns: SendContentResult struct - check error_code field (0 = success, negative = error)
|
|
|
|
|
## The result must be freed with destroy_send_content_result()
|
2026-01-29 01:50:41 +07:00
|
|
|
proc send_content*(
|
|
|
|
|
ctx: ContextHandle,
|
2026-02-06 13:41:20 -08:00
|
|
|
convo_id: ReprCString,
|
2026-01-29 01:50:41 +07:00
|
|
|
content: SliceUint8,
|
2026-02-06 13:41:20 -08:00
|
|
|
): SendContentResult {.importc, dynlib: CONVERSATIONS_LIB.}
|
2026-01-29 01:50:41 +07:00
|
|
|
|
2026-02-06 13:41:20 -08:00
|
|
|
## Handles an incoming payload
|
|
|
|
|
## Returns: HandlePayloadResult struct - check error_code field (0 = success, negative = error)
|
|
|
|
|
## This call does not always generate content. If content is zero bytes long then there
|
|
|
|
|
## is no data, and the convo_id should be ignored.
|
|
|
|
|
## The result must be freed with destroy_handle_payload_result()
|
2026-01-29 01:50:41 +07:00
|
|
|
proc handle_payload*(
|
|
|
|
|
ctx: ContextHandle,
|
|
|
|
|
payload: SliceUint8,
|
2026-02-06 13:41:20 -08:00
|
|
|
): HandlePayloadResult {.importc, dynlib: CONVERSATIONS_LIB.}
|
|
|
|
|
|
|
|
|
|
## Free the result from create_intro_bundle
|
|
|
|
|
proc destroy_intro_result*(result: CreateIntroResult) {.importc, dynlib: CONVERSATIONS_LIB.}
|
2026-01-29 01:50:41 +07:00
|
|
|
|
2026-01-29 01:38:08 +07:00
|
|
|
## Free the result from create_new_private_convo
|
|
|
|
|
proc destroy_convo_result*(result: NewConvoResult) {.importc, dynlib: CONVERSATIONS_LIB.}
|
|
|
|
|
|
2026-02-06 13:41:20 -08:00
|
|
|
## Free the result from send_content
|
|
|
|
|
proc destroy_send_content_result*(result: SendContentResult) {.importc, dynlib: CONVERSATIONS_LIB.}
|
|
|
|
|
|
|
|
|
|
## Free the result from handle_payload
|
|
|
|
|
proc destroy_handle_payload_result*(result: HandlePayloadResult) {.importc, dynlib: CONVERSATIONS_LIB.}
|
2026-01-29 01:38:08 +07:00
|
|
|
|
|
|
|
|
# ============================================================================
|
|
|
|
|
# Helper functions
|
|
|
|
|
# ============================================================================
|
|
|
|
|
|
|
|
|
|
## Create a SliceRefUint8 from a string
|
|
|
|
|
proc toSlice*(s: string): SliceUint8 =
|
|
|
|
|
if s.len == 0:
|
|
|
|
|
SliceUint8(`ptr`: nil, len: 0)
|
|
|
|
|
else:
|
|
|
|
|
SliceUint8(`ptr`: cast[ptr uint8](unsafeAddr s[0]), len: csize_t(s.len))
|
|
|
|
|
|
|
|
|
|
## Create a SliceRefUint8 from a seq[byte]
|
|
|
|
|
proc toSlice*(s: seq[byte]): SliceUint8 =
|
|
|
|
|
if s.len == 0:
|
|
|
|
|
SliceUint8(`ptr`: nil, len: 0)
|
|
|
|
|
else:
|
|
|
|
|
SliceUint8(`ptr`: cast[ptr uint8](unsafeAddr s[0]), len: csize_t(s.len))
|
|
|
|
|
|
|
|
|
|
## Convert a ReprCString to a Nim string
|
|
|
|
|
proc `$`*(s: ReprCString): string =
|
|
|
|
|
if s.ptr == nil or s.len == 0:
|
|
|
|
|
return ""
|
|
|
|
|
result = newString(s.len)
|
|
|
|
|
copyMem(addr result[0], s.ptr, s.len)
|
|
|
|
|
|
2026-02-06 13:41:20 -08:00
|
|
|
## Create a ReprCString from a Nim string for passing to FFI functions.
|
|
|
|
|
## WARNING: The returned ReprCString borrows from the input string.
|
|
|
|
|
## The input string must remain valid for the duration of the FFI call.
|
|
|
|
|
## cap is set to 0 to prevent Rust from attempting to deallocate Nim memory.
|
|
|
|
|
proc toReprCString*(s: string): ReprCString =
|
|
|
|
|
if s.len == 0:
|
|
|
|
|
ReprCString(`ptr`: nil, len: 0, cap: 0)
|
|
|
|
|
else:
|
|
|
|
|
ReprCString(`ptr`: cast[ptr char](unsafeAddr s[0]), len: csize_t(s.len), cap: 0)
|
|
|
|
|
|
2026-01-29 01:38:08 +07:00
|
|
|
## Convert a VecUint8 to a seq[byte]
|
|
|
|
|
proc toSeq*(v: VecUint8): seq[byte] =
|
|
|
|
|
if v.ptr == nil or v.len == 0:
|
|
|
|
|
return @[]
|
|
|
|
|
result = newSeq[byte](v.len)
|
|
|
|
|
copyMem(addr result[0], v.ptr, v.len)
|
|
|
|
|
|
|
|
|
|
## Access payloads from VecPayload
|
|
|
|
|
proc `[]`*(v: VecPayload, i: int): Payload =
|
|
|
|
|
assert i >= 0 and csize_t(i) < v.len
|
|
|
|
|
cast[ptr UncheckedArray[Payload]](v.ptr)[i]
|
|
|
|
|
|
|
|
|
|
## Get length of VecPayload
|
|
|
|
|
proc len*(v: VecPayload): int =
|
|
|
|
|
int(v.len)
|
2026-02-06 23:58:23 +07:00
|
|
|
|
2026-02-06 13:41:20 -08:00
|
|
|
## Iterator for VecPayload
|
|
|
|
|
iterator items*(v: VecPayload): Payload =
|
|
|
|
|
for i in 0 ..< v.len:
|
|
|
|
|
yield v[int(i)]
|
|
|
|
|
|
2026-02-06 23:58:23 +07:00
|
|
|
## Convert a string to seq[byte]
|
|
|
|
|
proc toBytes*(s: string): seq[byte] =
|
|
|
|
|
if s.len == 0:
|
|
|
|
|
return @[]
|
|
|
|
|
result = newSeq[byte](s.len)
|
|
|
|
|
copyMem(addr result[0], unsafeAddr s[0], s.len)
|