mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-02-09 16:33:10 +00:00
Nim FFI Wrapper for LibChat (#25)
* remove unneeded code * Update comment style * Update Nim side to support safer_ffi * Fix Introduction from impl * Updates
This commit is contained in:
parent
15bb395475
commit
1cb1ffc996
3
.gitignore
vendored
3
.gitignore
vendored
@ -24,6 +24,9 @@ target
|
||||
|
||||
# Compiled binary
|
||||
**/ffi_nim_example
|
||||
/nim-bindings/examples/pingpong
|
||||
/nim-bindings/libchat
|
||||
|
||||
# Temporary data folder
|
||||
tmp
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
crate-type = ["staticlib"]
|
||||
crate-type = ["staticlib","dylib"]
|
||||
|
||||
[dependencies]
|
||||
blake2.workspace = true
|
||||
|
||||
@ -71,8 +71,7 @@ pub fn create_new_private_convo(
|
||||
content: c_slice::Ref<'_, u8>,
|
||||
) -> NewConvoResult {
|
||||
// Convert input bundle to Introduction
|
||||
let s = String::from_utf8_lossy(&bundle).to_string();
|
||||
let Ok(intro) = Introduction::try_from(s) else {
|
||||
let Ok(intro) = Introduction::try_from(bundle.as_slice()) else {
|
||||
return NewConvoResult {
|
||||
error_code: ErrorCode::BadIntro as i32,
|
||||
convo_id: 0,
|
||||
|
||||
@ -31,12 +31,11 @@ impl Into<Vec<u8>> for Introduction {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Vec<u8>> for Introduction {
|
||||
impl TryFrom<&[u8]> for Introduction {
|
||||
type Error = ChatError;
|
||||
|
||||
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
|
||||
let str_value =
|
||||
String::from_utf8(value).map_err(|_| ChatError::BadParsing("Introduction"))?;
|
||||
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
|
||||
let str_value = String::from_utf8_lossy(value);
|
||||
let parts: Vec<&str> = str_value.splitn(3, ':').collect();
|
||||
|
||||
if parts[0] != "Bundle" {
|
||||
|
||||
8
nim-bindings/README.md
Normal file
8
nim-bindings/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
# Nim-bindings
|
||||
|
||||
A Nim wrapping class that exposes LibChat functionality.
|
||||
|
||||
|
||||
## Getting Started
|
||||
|
||||
`nimble pingpong` - Run the pingpong example.
|
||||
21
nim-bindings/conversations_example.nimble
Normal file
21
nim-bindings/conversations_example.nimble
Normal file
@ -0,0 +1,21 @@
|
||||
# Package
|
||||
|
||||
version = "0.1.0"
|
||||
author = "libchat"
|
||||
description = "Nim Bindings for LibChat"
|
||||
license = "MIT"
|
||||
srcDir = "src"
|
||||
bin = @["libchat"]
|
||||
|
||||
|
||||
# Dependencies
|
||||
|
||||
requires "nim >= 2.2.4"
|
||||
requires "results"
|
||||
|
||||
# Build Rust library before compiling Nim
|
||||
before build:
|
||||
exec "cargo build --release --manifest-path ../Cargo.toml"
|
||||
|
||||
task pingpong, "Run pingpong example":
|
||||
exec "nim c -r --path:src examples/pingpong.nim"
|
||||
22
nim-bindings/examples/pingpong.nim
Normal file
22
nim-bindings/examples/pingpong.nim
Normal file
@ -0,0 +1,22 @@
|
||||
import results
|
||||
|
||||
import ../src/libchat
|
||||
|
||||
proc pingpong() =
|
||||
|
||||
var raya = newConversationsContext()
|
||||
var saro = newConversationsContext()
|
||||
|
||||
|
||||
# Perform out of band Introduction
|
||||
let intro = raya.createIntroductionBundle().expect("[Raya] Couldn't create intro bundle")
|
||||
echo "Raya's Intro Bundle: ",intro
|
||||
|
||||
var (convo_sr, payload) = saro.createNewPrivateConvo(intro,"Hey Raya").expect("[Saro] Couldn't create convo")
|
||||
echo "ConvoHandle:: ", convo_sr
|
||||
echo "Payload:: ", payload
|
||||
|
||||
|
||||
|
||||
when isMainModule:
|
||||
pingpong()
|
||||
155
nim-bindings/src/bindings.nim
Normal file
155
nim-bindings/src/bindings.nim
Normal file
@ -0,0 +1,155 @@
|
||||
# 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 ConvoHandle* = uint32
|
||||
|
||||
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)
|
||||
PayloadResult* = object
|
||||
error_code*: int32
|
||||
payloads*: VecPayload
|
||||
|
||||
## Result from create_new_private_convo
|
||||
## error_code is 0 on success, negative on error (see ErrorCode)
|
||||
NewConvoResult* = object
|
||||
error_code*: int32
|
||||
convo_id*: uint32
|
||||
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
|
||||
## Returns: Number of bytes written to bundle_out, or negative error code
|
||||
proc create_intro_bundle*(
|
||||
ctx: ContextHandle,
|
||||
bundle_out: SliceUint8,
|
||||
): int32 {.importc, dynlib: CONVERSATIONS_LIB.}
|
||||
|
||||
## 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.}
|
||||
|
||||
## Free the result from create_new_private_convo
|
||||
proc destroy_convo_result*(result: NewConvoResult) {.importc, dynlib: CONVERSATIONS_LIB.}
|
||||
|
||||
## Free the PayloadResult
|
||||
proc destroy_payload_result*(result: PayloadResult) {.importc, dynlib: CONVERSATIONS_LIB.}
|
||||
|
||||
# ============================================================================
|
||||
# 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)
|
||||
|
||||
## 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)
|
||||
88
nim-bindings/src/libchat.nim
Normal file
88
nim-bindings/src/libchat.nim
Normal file
@ -0,0 +1,88 @@
|
||||
import results
|
||||
import bindings
|
||||
|
||||
type
|
||||
LibChat* = object
|
||||
handle: ContextHandle
|
||||
buffer_size: int
|
||||
|
||||
PayloadResult* = object
|
||||
address*: string
|
||||
data*: seq[uint8]
|
||||
|
||||
## Create a new conversations context
|
||||
proc newConversationsContext*(): LibChat =
|
||||
|
||||
result.handle = create_context()
|
||||
result.buffer_size = 256
|
||||
if result.handle.isNil:
|
||||
raise newException(IOError, "Failed to create context")
|
||||
|
||||
## Destroy the context and free resources
|
||||
proc destroy*(ctx: var LibChat) =
|
||||
|
||||
if not ctx.handle.isNil:
|
||||
destroy_context(ctx.handle)
|
||||
ctx.handle = nil
|
||||
|
||||
## Helper proc to create buffer of sufficient size
|
||||
proc getBuffer*(ctx: LibChat): seq[byte] =
|
||||
newSeq[byte](ctx.buffer_size)
|
||||
|
||||
## Generate a Introduction Bundle
|
||||
proc createIntroductionBundle*(ctx: LibChat): Result[string, string] =
|
||||
if ctx.handle == nil:
|
||||
return err("Context handle is nil")
|
||||
|
||||
var buffer = ctx.getBuffer()
|
||||
var slice = buffer.toSlice()
|
||||
let len = create_intro_bundle(ctx.handle, slice)
|
||||
|
||||
if len < 0:
|
||||
return err("Failed to create intro bundle: " & $len)
|
||||
|
||||
buffer.setLen(len)
|
||||
return ok(cast[string](buffer))
|
||||
|
||||
## Create a Private Convo
|
||||
proc createNewPrivateConvo*(ctx: LibChat, bundle: string, content: string): Result[(ConvoHandle, seq[PayloadResult]), string] =
|
||||
if ctx.handle == nil:
|
||||
return err("Context handle is nil")
|
||||
|
||||
if bundle.len == 0:
|
||||
return err("bundle is zero length")
|
||||
if content.len == 0:
|
||||
return err("content is zero length")
|
||||
|
||||
let res = bindings.create_new_private_convo(
|
||||
ctx.handle,
|
||||
bundle.toSlice(),
|
||||
content.toSlice()
|
||||
)
|
||||
|
||||
if res.error_code != 0:
|
||||
result = err("Failed to create private convo: " & $res.error_code)
|
||||
destroy_convo_result(res)
|
||||
return
|
||||
|
||||
# Convert payloads to Nim types
|
||||
var payloads = newSeq[PayloadResult](res.payloads.len)
|
||||
for i in 0 ..< res.payloads.len:
|
||||
let p = res.payloads[int(i)]
|
||||
payloads[int(i)] = PayloadResult(
|
||||
address: $p.address,
|
||||
data: p.data.toSeq()
|
||||
)
|
||||
|
||||
let convoId = res.convo_id
|
||||
|
||||
# Free the result
|
||||
destroy_convo_result(res)
|
||||
|
||||
return ok((convoId, payloads))
|
||||
|
||||
|
||||
proc `=destroy`(x: var LibChat) =
|
||||
# Automatically free handle when the destructor is called
|
||||
if x.handle != nil:
|
||||
x.destroy()
|
||||
Loading…
x
Reference in New Issue
Block a user