Remove legacy nim bindings

This commit is contained in:
Jazz Turner-Baggs 2026-03-30 14:17:16 -07:00
parent e5cefdc02d
commit aa0d51b35b
No known key found for this signature in database
8 changed files with 0 additions and 715 deletions

View File

@ -1,3 +0,0 @@
nimble.develop
nimble.paths
nimbledeps

View File

@ -1,8 +0,0 @@
# Nim-bindings
A Nim wrapping class that exposes LibChat functionality.
## Getting Started
`nimble pingpong` - Run the pingpong example.

View File

@ -1,30 +0,0 @@
# 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"
proc buildRust() =
exec "cargo build --release --manifest-path ../Cargo.toml"
# Build Rust library before compiling Nim
before build:
buildRust()
task pingpong, "Run pingpong example":
buildRust()
exec "nim c -r --path:src --passL:../target/release/liblibchat.a --passL:-lm examples/pingpong.nim"
task test, "Run comprehensive all-endpoints test":
buildRust()
exec "nim c -r --path:src --passL:../target/release/liblibchat.a --passL:-lm tests/test_all_endpoints.nim"

View File

@ -1,4 +0,0 @@
# begin Nimble config (version 2)
when withDir(thisDir(), system.fileExists("nimble.paths")):
include "nimble.paths"
# end Nimble config

View File

@ -1,45 +0,0 @@
import options
import results
import std/strutils
import ../src/libchat
## Convert a string to seq[byte]
proc encode*(s: string): seq[byte] =
if s.len == 0:
return @[]
result = newSeq[byte](s.len)
copyMem(addr result[0], unsafeAddr s[0], s.len)
proc pingpong() =
var raya = newConversationsContext("raya")
var saro = newConversationsContext("saro")
# 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, payloads) = saro.createNewPrivateConvo(intro, encode("Hey Raya")).expect("[Saro] Couldn't create convo")
echo "ConvoId:: ", convo_sr
echo "Payload:: ", payloads
## Send Payloads to Raya
for p in payloads:
let res = raya.handlePayload(p.data)
if res.isOk:
let opt = res.get()
if opt.isSome:
let content_result = opt.get()
echo "RecvContent: ", content_result.conversationId, " ", content_result.data
else:
echo "Failed to handle payload: ", res.error
echo "Done"
when isMainModule:
pingpong()

View File

@ -1,232 +0,0 @@
# Nim FFI bindings for libchat conversations library
# 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
## Vector of Payloads returned by safer_ffi functions
VecString* = object
`ptr`*: ptr ReprCString
len*: csize_t
cap*: csize_t
## Result structure for create_intro_bundle
## error_code is 0 on success, negative on error (see ErrorCode)
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
error_code*: int32
payloads*: VecPayload
## 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
is_new_convo*: bool
## Result from create_new_private_convo
## error_code is 0 on success, negative on error (see ErrorCode)
NewConvoResult* = object
error_code*: int32
convo_id*: ReprCString
payloads*: VecPayload
## Result from list_conversations
## error_code is 0 on success, negative on error (see ErrorCode)
ListConvoResult* = object
error_code*: int32
convo_ids*: VecString
# FFI function imports
## Creates a new libchat Context
## Returns: Opaque handle to the context. Must be freed with destroy_context()
proc create_context*(name: ReprCString): ContextHandle {.importc.}
## Returns the friendly name of the context's identity
## The result must be freed by the caller (repr_c::String ownership transfers)
proc installation_name*(ctx: ContextHandle): ReprCString {.importc.}
## 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.}
## Free a ReprCString returned by any of the FFI functions
## - s must be an owned ReprCString value returned from an FFI function
## - s must not be used after this call
proc destroy_string*(s: ReprCString) {.importc.}
## Creates an intro bundle for sharing with other users
## Returns: CreateIntroResult struct - check error_code field (0 = success, negative = error)
## The result must be freed with destroy_intro_result()
proc create_intro_bundle*(
ctx: ContextHandle,
): CreateIntroResult {.importc.}
## 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.}
## Get the available conversation identifers.
## Returns: ListConvoResult struct - check error_code field (0 = success, negative = error)
## The result must be freed with destroy_list_result()
proc list_conversations*(
ctx: ContextHandle,
): ListConvoResult {.importc.}
## Sends content to an existing conversation
## Returns: SendContentResult struct - check error_code field (0 = success, negative = error)
## The result must be freed with destroy_send_content_result()
proc send_content*(
ctx: ContextHandle,
convo_id: ReprCString,
content: SliceUint8,
): SendContentResult {.importc.}
## 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()
proc handle_payload*(
ctx: ContextHandle,
payload: SliceUint8,
): HandlePayloadResult {.importc.}
## Free the result from create_intro_bundle
proc destroy_intro_result*(result: CreateIntroResult) {.importc.}
## Free the result from create_new_private_convo
proc destroy_convo_result*(result: NewConvoResult) {.importc.}
## Free the result from list_conversation
proc destroy_list_result*(result: ListConvoResult) {.importc.}
## Free the result from send_content
proc destroy_send_content_result*(result: SendContentResult) {.importc.}
## Free the result from handle_payload
proc destroy_handle_payload_result*(result: HandlePayloadResult) {.importc.}
# ============================================================================
# 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)
## 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)
## Convert a VecUint8 to a seq[string]
proc toSeq*(v: VecString): seq[string] =
if v.ptr == nil or v.len == 0:
return @[]
result = newSeq[string](v.len)
let arr = cast[ptr UncheckedArray[ReprCString]](v.ptr)
for i in 0 ..< int(v.len):
result[i] = $arr[i]
## 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)
## Iterator for VecPayload
iterator items*(v: VecPayload): Payload =
for i in 0 ..< v.len:
yield v[int(i)]
## 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)

View File

@ -1,135 +0,0 @@
import std/options
import std/sequtils
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*(name: string): LibChat =
result.handle = create_context(name.toReprCString)
result.buffer_size = 256
if result.handle.isNil:
raise newException(IOError, "Failed to create context")
## Get the friendly name of this context's installation
proc getInstallationName*(ctx: LibChat): string =
if ctx.handle == nil:
return ""
let name = installation_name(ctx.handle)
defer: destroy_string(name)
result = $name
## 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[seq[byte], string] =
if ctx.handle == nil:
return err("Context handle is nil")
let res = create_intro_bundle(ctx.handle)
defer: destroy_intro_result(res)
if res.error_code != ErrNone:
return err("Failed to create intro bundle: " & $res.error_code)
return ok(res.intro_bytes.toSeq())
## Create a Private Convo
proc createNewPrivateConvo*(ctx: LibChat, bundle: seq[byte], content: seq[byte]): Result[(string, 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())
defer: destroy_convo_result(res)
if res.error_code != 0:
return err("Failed to create private convo: " & $res.error_code)
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())
return ok(($res.convo_id, payloads))
proc listConversations*(ctx: LibChat): Result[seq[string], string] =
if ctx.handle == nil:
return err("Context handle is nil")
let res = bindings.list_conversations(ctx.handle)
if res.error_code != 0:
result = err("Failed to list conversations: " & $res.error_code)
destroy_list_result(res)
return
ok(res.convo_ids.toSeq())
## Send content to an existing conversation
proc sendContent*(ctx: LibChat, convoId: string, content: seq[byte]): Result[seq[PayloadResult], string] =
if ctx.handle == nil:
return err("Context handle is nil")
if content.len == 0:
return err("content is zero length")
let res = bindings.send_content(ctx.handle, convoId.toReprCString, content.toSlice())
defer: destroy_send_content_result(res)
if res.error_code != 0:
return err("Failed to send content: " & $res.error_code)
let payloads = res.payloads.toSeq().mapIt(PayloadResult(address: $it.address, data: it.data.toSeq()))
return ok(payloads)
type
ContentResult* = object
conversationId*: string
data*: seq[uint8]
isNewConvo*: bool
## Handle an incoming payload and decrypt content
proc handlePayload*(ctx: LibChat, payload: seq[byte]): Result[Option[ContentResult], string] =
if ctx.handle == nil:
return err("Context handle is nil")
if payload.len == 0:
return err("payload is zero length")
let res = bindings.handle_payload(ctx.handle, payload.toSlice())
defer: destroy_handle_payload_result(res)
if res.error_code != ErrNone:
return err("Failed to handle payload: " & $res.error_code)
let content = res.content.toSeq()
if content.len == 0:
return ok(none(ContentResult))
return ok(some(ContentResult(
conversationId: $res.convo_id,
data: content,
isNewConvo: res.is_new_convo
)))

View File

@ -1,258 +0,0 @@
# Comprehensive test for all FFI procs declared in bindings.nim.
#
# Design intent: By importing `bindings` directly and calling every importc
# proc at least once, the linker is forced to include ALL symbol references.
# This prevents link-time optimizations from stripping unused symbols and
# catches both link-time crashes (missing symbols) and runtime crashes
# (wrong ABI, segfaults on use).
import bindings
# ---------------------------------------------------------------------------
# Assertion helper
# ---------------------------------------------------------------------------
proc check(cond: bool, msg: string) =
if not cond:
echo "FAIL: ", msg
quit(1)
echo "OK: ", msg
# ---------------------------------------------------------------------------
# Section 1: Helper proc coverage
# ---------------------------------------------------------------------------
proc testHelperProcs() =
echo "\n--- testHelperProcs ---"
# toSlice(string) — non-empty and empty branches
let s = "hello"
let sl = toSlice(s)
check(sl.len == 5, "toSlice(string): correct len")
check(sl.`ptr` != nil, "toSlice(non-empty string): non-nil ptr")
let emptySl = toSlice("")
check(emptySl.len == 0, "toSlice(empty string): len == 0")
check(emptySl.`ptr` == nil, "toSlice(empty string): ptr == nil")
# toSlice(seq[byte]) — non-empty and empty branches
let b: seq[byte] = @[0x61'u8, 0x62'u8, 0x63'u8]
let bSl = toSlice(b)
check(bSl.len == 3, "toSlice(seq[byte]): correct len")
check(bSl.`ptr` != nil, "toSlice(non-empty seq[byte]): non-nil ptr")
let emptyBSl = toSlice(newSeq[byte](0))
check(emptyBSl.len == 0, "toSlice(empty seq[byte]): len == 0")
check(emptyBSl.`ptr` == nil, "toSlice(empty seq[byte]): ptr == nil")
# toReprCString(string) and $(ReprCString) round-trip
let name = "testname"
let rcs = toReprCString(name)
check(rcs.len == csize_t(name.len), "toReprCString: correct len")
check(rcs.cap == 0, "toReprCString: cap == 0 (prevents Rust dealloc of Nim memory)")
check(rcs.`ptr` != nil, "toReprCString: non-nil ptr")
check($rcs == name, "$(ReprCString): round-trips to original string")
let emptyRcs = toReprCString("")
check(emptyRcs.len == 0, "toReprCString(empty): len == 0")
check($emptyRcs == "", "$(empty ReprCString): returns empty string")
# toBytes(string)
let bs = toBytes("abc")
check(bs.len == 3, "toBytes: correct length")
check(bs[0] == 0x61'u8, "toBytes: correct first byte")
let emptyBs = toBytes("")
check(emptyBs.len == 0, "toBytes(empty): empty seq")
# ---------------------------------------------------------------------------
# Section 2: create_context / installation_name / destroy_context
# ---------------------------------------------------------------------------
proc testContextLifecycle() =
echo "\n--- testContextLifecycle ---"
let ctx = create_context(toReprCString("lifecycle-test"))
check(ctx != nil, "create_context: returns non-nil handle")
let iname = installation_name(ctx)
defer: destroy_string(iname)
let inameStr = $iname
check(inameStr.len > 0, "installation_name: returns non-empty name")
echo " installation name: ", inameStr
destroy_context(ctx)
echo " destroy_context: no crash"
# ---------------------------------------------------------------------------
# Section 3: Full two-party conversation flow
# ---------------------------------------------------------------------------
# Exercises: create_intro_bundle, create_new_private_convo, handle_payload,
# send_content, and all four destroy_* procs.
# VecPayload helpers ([], len, items) are also exercised here.
proc testFullConversationFlow() =
echo "\n--- testFullConversationFlow ---"
let aliceCtx = create_context(toReprCString("alice"))
check(aliceCtx != nil, "Alice: create_context non-nil")
let bobCtx = create_context(toReprCString("bob"))
check(bobCtx != nil, "Bob: create_context non-nil")
# --- create_intro_bundle ---
var bobIntroRes = create_intro_bundle(bobCtx)
check(bobIntroRes.error_code == ErrNone,
"create_intro_bundle: error_code == ErrNone")
check(bobIntroRes.intro_bytes.len > 0,
"create_intro_bundle: intro_bytes non-empty")
# toSeq(VecUint8)
let introBytes = toSeq(bobIntroRes.intro_bytes)
check(introBytes.len > 0, "toSeq(VecUint8): produces non-empty seq")
# destroy_intro_result
destroy_intro_result(bobIntroRes)
echo " destroy_intro_result: no crash"
# --- create_new_private_convo ---
var convoRes = create_new_private_convo(
aliceCtx,
toSlice(introBytes),
toSlice("Hello, Bob!")
)
check(convoRes.error_code == ErrNone,
"create_new_private_convo: error_code == ErrNone")
let aliceConvoId = $convoRes.convo_id
check(aliceConvoId.len > 0, "create_new_private_convo: convo_id non-empty")
echo " Alice-Bob convo_id: ", aliceConvoId
# len(VecPayload)
let numPayloads = len(convoRes.payloads)
check(numPayloads > 0, "len(VecPayload): > 0 payloads in new convo")
# [](VecPayload, int): subscript access
let firstPayload = convoRes.payloads[0]
check(firstPayload.data.len > 0, "VecPayload[0].data: non-empty")
check(firstPayload.address.len > 0, "VecPayload[0].address: non-empty")
echo " first payload address: ", $firstPayload.address
# items(VecPayload): collect bytes before destroy
var payloadDatas: seq[seq[byte]] = @[]
var iterCount = 0
for p in convoRes.payloads:
payloadDatas.add(toSeq(p.data))
inc iterCount
check(iterCount == numPayloads,
"items(VecPayload): iterator yields all payloads")
# destroy_convo_result
destroy_convo_result(convoRes)
echo " destroy_convo_result: no crash"
# --- handle_payload ---
var bobSawContent = false
var bobConvoId = ""
for pData in payloadDatas:
var hp = handle_payload(bobCtx, toSlice(pData))
check(hp.error_code == ErrNone, "handle_payload: error_code == ErrNone")
let content = toSeq(hp.content)
if content.len > 0:
bobConvoId = $hp.convo_id
check(bobConvoId.len > 0,
"handle_payload: convo_id non-empty when content present")
if not bobSawContent:
check(hp.is_new_convo,
"handle_payload: is_new_convo == true on first contact")
bobSawContent = true
echo " Bob received content in convo: ", bobConvoId
destroy_handle_payload_result(hp)
check(bobSawContent, "handle_payload: Bob received Alice's opening message")
echo " destroy_handle_payload_result: no crash"
# --- send_content ---
var sendRes = send_content(
aliceCtx,
toReprCString(aliceConvoId),
toSlice("How are you, Bob?")
)
check(sendRes.error_code == ErrNone,
"send_content: error_code == ErrNone for valid convo_id")
check(len(sendRes.payloads) > 0,
"send_content: returns at least one payload")
var sendPayloadDatas: seq[seq[byte]] = @[]
for p in sendRes.payloads:
sendPayloadDatas.add(toSeq(p.data))
# destroy_send_content_result
destroy_send_content_result(sendRes)
echo " destroy_send_content_result: no crash"
# Bob handles follow-up payloads
for pData in sendPayloadDatas:
var hp2 = handle_payload(bobCtx, toSlice(pData))
check(hp2.error_code == ErrNone,
"handle_payload: Bob handles send_content payload without error")
destroy_handle_payload_result(hp2)
destroy_context(aliceCtx)
destroy_context(bobCtx)
echo " both contexts destroyed: no crash"
# ---------------------------------------------------------------------------
# Section 4: Error-case coverage
# ---------------------------------------------------------------------------
# Exercises destroy_* on error results (empty/null Vecs) to confirm they
# do not crash.
proc testErrorCases() =
echo "\n--- testErrorCases ---"
let ctx = create_context(toReprCString("error-tester"))
check(ctx != nil, "error-tester: create_context non-nil")
# send_content with a nonexistent convo_id must fail
var badSend = send_content(
ctx,
toReprCString("00000000-0000-0000-0000-nonexistent"),
toSlice("payload")
)
check(badSend.error_code != ErrNone,
"send_content(bad convo_id): error_code != ErrNone")
echo " send_content(bad convo_id) error_code: ", badSend.error_code
# Destroy error result to confirm destroy handles empty VecPayload
destroy_send_content_result(badSend)
echo " destroy_send_content_result(error result): no crash"
# create_new_private_convo with garbage bytes must fail with ErrBadIntro
let badIntro: seq[byte] = @[0xDE'u8, 0xAD'u8, 0xBE'u8, 0xEF'u8]
var badConvo = create_new_private_convo(
ctx,
toSlice(badIntro),
toSlice("content")
)
check(badConvo.error_code == ErrBadIntro,
"create_new_private_convo(bad intro): error_code == ErrBadIntro")
destroy_convo_result(badConvo)
echo " destroy_convo_result(error result): no crash"
destroy_context(ctx)
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
when isMainModule:
echo "=== test_all_endpoints: begin ==="
testHelperProcs()
testContextLifecycle()
testFullConversationFlow()
testErrorCases()
echo "\n=== ALL TESTS PASSED ==="