-
Notifications
You must be signed in to change notification settings - Fork 1
Nim FFI Wrapper for LibChat #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # Nim-bindings | ||
|
|
||
| A Nim wrapping class that exposes LibChat functionality. | ||
|
|
||
|
|
||
| ## Getting Started | ||
|
|
||
| `nimble pingpong` - Run the pingpong example. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import results | ||
|
|
||
| import ../src/libchat | ||
|
|
||
| proc pingpong() = | ||
|
|
||
| var raya = newConversationsContext() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be helpful for me (maybe also others) to reason about the demo if using alice and bob...
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've talked else where about why Alice and Bob are not the right fit. I propose an official specification here : logos-messaging/specs#99 |
||
| 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() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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() |
Uh oh!
There was an error while loading. Please reload this page.