Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ target

# Compiled binary
**/ffi_nim_example
/nim-bindings/examples/pingpong
/nim-bindings/libchat

# Temporary data folder
tmp

2 changes: 1 addition & 1 deletion conversations/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["staticlib"]
crate-type = ["staticlib","dylib"]

[dependencies]
blake2.workspace = true
Expand Down
3 changes: 1 addition & 2 deletions conversations/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 3 additions & 4 deletions conversations/src/inbox/introduction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down
8 changes: 8 additions & 0 deletions nim-bindings/README.md
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.
21 changes: 21 additions & 0 deletions nim-bindings/conversations_example.nimble
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"
22 changes: 22 additions & 0 deletions nim-bindings/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()
Copy link
Contributor

Choose a reason for hiding this comment

The 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...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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()
155 changes: 155 additions & 0 deletions nim-bindings/src/bindings.nim
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)
88 changes: 88 additions & 0 deletions nim-bindings/src/libchat.nim
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()