feat(skelleton): skelleton with bindings

This commit is contained in:
pablo 2025-06-08 11:19:45 +03:00
parent 75a22367b9
commit 010a984c3e
No known key found for this signature in database
GPG Key ID: 78F35FCC60FDC63A
12 changed files with 658 additions and 0 deletions

54
Makefile Normal file
View File

@ -0,0 +1,54 @@
# ChatSDK Makefile
# This builds the complete chain: Nim -> C bindings -> Go bindings -> Go example
.PHONY: all clean build-nim build-c build-go run-go-example help
# Default target
all: build-nim build-c build-go
# Help target
help:
@echo "ChatSDK Build System"
@echo "===================="
@echo "Available targets:"
@echo " all - Build everything (Nim + C + Go)"
@echo " build-nim - Build Nim library"
@echo " build-c - Build C bindings (shared library)"
@echo " build-go - Build Go bindings"
@echo " run-go-example - Run the Go example application"
@echo " clean - Clean all build artifacts"
@echo " help - Show this help message"
# Build Nim library
build-nim:
@echo "Building Nim library..."
cd src && nim c --app:lib --opt:speed --mm:arc --out:../bindings/c-bindings/libchatsdk.so chat_sdk.nim
# Build C bindings
build-c: build-nim
@echo "C bindings ready (built with Nim)"
# Build Go bindings (just verify they compile)
build-go: build-c
@echo "Building Go bindings..."
cd bindings/go-bindings && go build .
# Run Go example
run-go-example: build-go
@echo "Running Go example..."
cd examples/go-app && \
LD_LIBRARY_PATH=../../bindings/c-bindings:$$LD_LIBRARY_PATH \
go run main.go
# Clean all build artifacts
clean:
@echo "Cleaning build artifacts..."
rm -f bindings/c-bindings/*.so bindings/c-bindings/*.a
rm -rf src/nimcache bindings/c-bindings/nimcache
cd bindings/go-bindings && go clean
cd examples/go-app && go clean
# Test the Nim library directly
test-nim:
@echo "Testing Nim library directly..."
cd src && nim r chat_sdk.nim

19
README.md Normal file
View File

@ -0,0 +1,19 @@
# Chat SDK
## Quick Start
### Build Everything
```bash
# Build the complete chain: Nim → C → Go
make all
# Or step by step:
make build-nim # Build Nim library to shared library
make build-c # Prepare C bindings
make build-go # Verify Go bindings compile
```
### Run the Go Example
```bash
make run-go-example
```

View File

@ -0,0 +1,35 @@
CC = gcc
CFLAGS = -Wall -Wextra -fPIC
NIMFLAGS = --app:lib --threads:on --gc:orc
# Directories
SRC_DIR = ../../src
BUILD_DIR = .
# Targets
SHARED_LIB = libchatsdk.so
STATIC_LIB = libchatsdk.a
HEADER = chatsdk.h
.PHONY: all clean shared static
all: shared static
shared: $(SHARED_LIB)
static: $(STATIC_LIB)
$(SHARED_LIB): $(SRC_DIR)/chat_sdk.nim $(HEADER)
cd $(SRC_DIR) && nim c $(NIMFLAGS) --out:../bindings/c-bindings/$(SHARED_LIB) chat_sdk.nim
$(STATIC_LIB): $(SRC_DIR)/chat_sdk.nim $(HEADER)
cd $(SRC_DIR) && nim c $(NIMFLAGS) --app:staticLib --out:../bindings/c-bindings/$(STATIC_LIB) chat_sdk.nim
clean:
rm -f $(SHARED_LIB) $(STATIC_LIB) *.o *.so *.a
rm -rf nimcache
install: $(SHARED_LIB) $(HEADER)
sudo cp $(SHARED_LIB) /usr/local/lib/
sudo cp $(HEADER) /usr/local/include/
sudo ldconfig

View File

@ -0,0 +1,64 @@
#ifndef CHATSDK_H
#define CHATSDK_H
#ifdef __cplusplus
extern "C" {
#endif
// Forward declaration for ChatSDK
typedef struct ChatSDK ChatSDK;
// Storage interface function pointer types
typedef int (*StoreMessageProc)(const char* id, const char* message, void* userData);
typedef const char* (*GetMessageProc)(const char* id, void* userData);
/**
* Send a message through the ChatSDK (standalone version)
* @param message The message to send as a null-terminated string
* @return 0 on success, non-zero on error
*/
int sendMessageCString(const char* message);
/**
* Create a new ChatSDK instance with storage callbacks
* @param storeProc Function pointer for storing messages
* @param getProc Function pointer for retrieving messages
* @param userData Optional user data pointer
* @return Pointer to ChatSDK instance or NULL on error
*/
ChatSDK* newChatSDKC(StoreMessageProc storeProc, GetMessageProc getProc, void* userData);
/**
* Free a ChatSDK instance
* @param sdk Pointer to ChatSDK instance to free
*/
void freeChatSDKC(ChatSDK* sdk);
/**
* Send a message through a ChatSDK instance
* @param sdk Pointer to ChatSDK instance
* @param id Message ID
* @param message The message to send
* @return 0 on success, non-zero on error
*/
int sendMessageSDKC(ChatSDK* sdk, const char* id, const char* message);
/**
* Get a message from a ChatSDK instance
* @param sdk Pointer to ChatSDK instance
* @param id Message ID to retrieve
* @return Message string (caller must call freeCString) or NULL if not found
*/
const char* getMessageSDKC(ChatSDK* sdk, const char* id);
/**
* Free a C string allocated by the library
* @param str String to free
*/
void freeCString(const char* str);
#ifdef __cplusplus
}
#endif
#endif // CHATSDK_H

BIN
bindings/c-bindings/libchatsdk.so Executable file

Binary file not shown.

View File

@ -0,0 +1,56 @@
package chatsdk
/*
#include <stdlib.h>
*/
import "C"
import (
"unsafe"
)
// goStoreMessage is called from C to store a message using the Go Store interface
//
//export goStoreMessage
func goStoreMessage(cID *C.char, cMessage *C.char, userData unsafe.Pointer) C.int {
if cID == nil || cMessage == nil {
return 1 // Error
}
store := getStoreFromUserData(userData)
if store == nil {
return 1 // Error
}
id := C.GoString(cID)
message := C.GoString(cMessage)
success := store.StoreMessage(id, message)
if success {
return 0 // Success
}
return 1 // Error
}
// goGetMessage is called from C to retrieve a message using the Go Store interface
//
//export goGetMessage
func goGetMessage(cID *C.char, userData unsafe.Pointer) *C.char {
if cID == nil {
return nil
}
store := getStoreFromUserData(userData)
if store == nil {
return nil
}
id := C.GoString(cID)
message := store.GetMessage(id)
if message == "" {
return nil
}
// Allocate C string - Nim side will free this
return C.CString(message)
}

View File

@ -0,0 +1,169 @@
package chatsdk
/*
#cgo CFLAGS: -I../c-bindings
#cgo LDFLAGS: -L../c-bindings -lchatsdk
#include "chatsdk.h"
#include <stdlib.h>
// Forward declarations for the Go callback functions
int goStoreMessage(const char* id, const char* message, void* userData);
const char* goGetMessage(const char* id, void* userData);
*/
import "C"
import (
"errors"
"runtime"
"sync"
"unsafe"
)
// Store interface that Go implementations must satisfy
type Store interface {
StoreMessage(id, message string) bool
GetMessage(id string) string
}
// ChatSDK represents a chat SDK instance with storage capabilities
type ChatSDK struct {
cSDK *C.ChatSDK
store Store
closed bool
mu sync.RWMutex
}
// Global registry to map C callback calls back to Go Store implementations
var (
storeRegistry = make(map[uintptr]Store)
registryMu sync.RWMutex
nextID uintptr = 1
)
// SendMessage sends a message through the ChatSDK (standalone version)
func SendMessage(message string) error {
cMessage := C.CString(message)
defer C.free(unsafe.Pointer(cMessage))
result := C.sendMessageCString(cMessage)
if result != 0 {
return errors.New("failed to send message")
}
return nil
}
// NewChatSDK creates a new ChatSDK instance with the provided store implementation
func NewChatSDK(store Store) (*ChatSDK, error) {
if store == nil {
return nil, errors.New("store cannot be nil")
}
// Register the store implementation
registryMu.Lock()
storeID := nextID
nextID++
storeRegistry[storeID] = store
registryMu.Unlock()
// Create the C SDK instance with callback function pointers
userData := unsafe.Pointer(uintptr(storeID))
cSDK := C.newChatSDKC(
C.StoreMessageProc(C.goStoreMessage),
C.GetMessageProc(C.goGetMessage),
userData,
)
if cSDK == nil {
// Clean up registry on failure
registryMu.Lock()
delete(storeRegistry, storeID)
registryMu.Unlock()
return nil, errors.New("failed to create ChatSDK instance")
}
sdk := &ChatSDK{
cSDK: cSDK,
store: store,
}
// Set finalizer to ensure cleanup
runtime.SetFinalizer(sdk, (*ChatSDK).Close)
return sdk, nil
}
// SendMessage sends a message through this ChatSDK instance
func (sdk *ChatSDK) SendMessage(id, message string) error {
sdk.mu.RLock()
defer sdk.mu.RUnlock()
if sdk.closed {
return errors.New("ChatSDK instance is closed")
}
cID := C.CString(id)
cMessage := C.CString(message)
defer C.free(unsafe.Pointer(cID))
defer C.free(unsafe.Pointer(cMessage))
result := C.sendMessageSDKC(sdk.cSDK, cID, cMessage)
if result != 0 {
return errors.New("failed to send message")
}
return nil
}
// GetMessage retrieves a message by ID through this ChatSDK instance
func (sdk *ChatSDK) GetMessage(id string) (string, error) {
sdk.mu.RLock()
defer sdk.mu.RUnlock()
if sdk.closed {
return "", errors.New("ChatSDK instance is closed")
}
cID := C.CString(id)
defer C.free(unsafe.Pointer(cID))
cResult := C.getMessageSDKC(sdk.cSDK, cID)
if cResult == nil {
return "", nil // Message not found
}
result := C.GoString(cResult)
C.freeCString(cResult) // Free the string allocated by Nim
return result, nil
}
// Close frees the ChatSDK instance and cleans up resources
func (sdk *ChatSDK) Close() error {
sdk.mu.Lock()
defer sdk.mu.Unlock()
if sdk.closed {
return nil
}
if sdk.cSDK != nil {
C.freeChatSDKC(sdk.cSDK)
sdk.cSDK = nil
}
sdk.closed = true
runtime.SetFinalizer(sdk, nil)
return nil
}
// getStoreFromUserData retrieves a Store implementation from userData pointer
func getStoreFromUserData(userData unsafe.Pointer) Store {
if userData == nil {
return nil
}
storeID := uintptr(userData)
registryMu.RLock()
store := storeRegistry[storeID]
registryMu.RUnlock()
return store
}

View File

@ -0,0 +1,3 @@
module github.com/waku-org/nim-chat-sdk/bindings/go-bindings
go 1.24

View File

@ -10,3 +10,9 @@ srcDir = "src"
# Dependencies
requires "nim >= 2.0.0"
task buildSharedLib, "Build shared library for C bindings":
exec "nim c --app:lib --out:../bindings/c-bindings/libchatsdk.so src/chat_sdk.nim"
task buildStaticLib, "Build static library for C bindings":
exec "nim c --app:staticLib --out:../bindings/c-bindings/libchatsdk.a src/chat_sdk.nim"

7
examples/go-app/go.mod Normal file
View File

@ -0,0 +1,7 @@
module go-app
replace github.com/waku-org/nim-chat-sdk/bindings/go-bindings => ../../bindings/go-bindings
require github.com/waku-org/nim-chat-sdk/bindings/go-bindings v0.0.0-00010101000000-000000000000
go 1.24

137
examples/go-app/main.go Normal file
View File

@ -0,0 +1,137 @@
package main
import (
"fmt"
"log"
"sync"
chatsdk "github.com/waku-org/nim-chat-sdk/bindings/go-bindings"
)
// SimpleStore implements the chatsdk.Store interface with in-memory storage
type SimpleStore struct {
messages map[string]string
mu sync.RWMutex
}
// NewSimpleStore creates a new SimpleStore instance
func NewSimpleStore() *SimpleStore {
return &SimpleStore{
messages: make(map[string]string),
}
}
// StoreMessage stores a message with the given ID
func (s *SimpleStore) StoreMessage(id, message string) bool {
s.mu.Lock()
defer s.mu.Unlock()
fmt.Printf("📦 Storing message [%s]: %s\n", id, message)
s.messages[id] = message
return true
}
// GetMessage retrieves a message by ID
func (s *SimpleStore) GetMessage(id string) string {
s.mu.RLock()
defer s.mu.RUnlock()
message, exists := s.messages[id]
if exists {
fmt.Printf("📤 Retrieved message [%s]: %s\n", id, message)
return message
}
fmt.Printf("❌ Message not found [%s]\n", id)
return ""
}
// ListAllMessages shows all stored messages
func (s *SimpleStore) ListAllMessages() {
s.mu.RLock()
defer s.mu.RUnlock()
fmt.Println("\n📋 All stored messages:")
if len(s.messages) == 0 {
fmt.Println(" (no messages stored)")
return
}
for id, message := range s.messages {
fmt.Printf(" [%s]: %s\n", id, message)
}
}
func main() {
fmt.Println("ChatSDK Go Example - Enhanced with Storage")
fmt.Println("==========================================")
// Test 1: Original standalone API (backward compatibility)
fmt.Println("\n🔸 Testing standalone API (backward compatibility):")
messages := []string{
"Hello from standalone API!",
"This message won't be stored",
}
for i, msg := range messages {
fmt.Printf("Sending standalone message #%d: %s\n", i+1, msg)
if err := chatsdk.SendMessage(msg); err != nil {
log.Printf("Error: %v", err)
} else {
fmt.Println("✓ Message sent successfully")
}
}
// Test 2: ChatSDK object with Store interface
fmt.Println("\n🔸 Testing ChatSDK object with Store interface:")
// Create a store implementation
store := NewSimpleStore()
// Create ChatSDK instance with the store
sdk, err := chatsdk.NewChatSDK(store)
if err != nil {
log.Fatalf("Failed to create ChatSDK: %v", err)
}
defer sdk.Close()
// Send messages with IDs (they will be stored)
testMessages := map[string]string{
"msg1": "Hello from ChatSDK object!",
"msg2": "This message will be stored and can be retrieved",
"msg3": "Nim ❤️ Go with storage interface working!",
"msg4": "Another stored message with a longer ID",
}
fmt.Println("\n📤 Sending messages with storage:")
for id, message := range testMessages {
fmt.Printf("Sending message [%s]: %s\n", id, message)
if err := sdk.SendMessage(id, message); err != nil {
log.Printf("Error sending message: %v", err)
} else {
fmt.Println("✓ Message sent and stored successfully")
}
fmt.Println()
}
// Test message retrieval
fmt.Println("\n📥 Testing message retrieval:")
testIDs := []string{"msg1", "msg2", "msg3", "msg4", "nonexistent"}
for _, id := range testIDs {
fmt.Printf("Retrieving message [%s]...\n", id)
message, err := sdk.GetMessage(id)
if err != nil {
log.Printf("Error retrieving message: %v", err)
} else if message != "" {
fmt.Printf("✓ Found: %s\n", message)
} else {
fmt.Printf("❌ Message not found\n")
}
fmt.Println()
}
// Show all stored messages
store.ListAllMessages()
fmt.Println("\n✅ Example completed successfully!")
}

View File

@ -1,3 +1,111 @@
import std/[times]
# Storage interface function pointer types
type
StoreMessageProc* = proc(id: cstring, message: cstring, userData: pointer): cint {.cdecl.}
GetMessageProc* = proc(id: cstring, userData: pointer): cstring {.cdecl.}
# ChatSDK object
type
ChatSDK* = object
storeCallback: StoreMessageProc
getCallback: GetMessageProc
userData: pointer # For Go-side data if needed
# Create a new ChatSDK instance
proc newChatSDK*(storeProc: StoreMessageProc, getProc: GetMessageProc, userData: pointer = nil): ChatSDK =
ChatSDK(
storeCallback: storeProc,
getCallback: getProc,
userData: userData
)
# Send message method for ChatSDK
proc sendMessage*(sdk: ChatSDK, id: string, message: string): bool =
## Sends a message by printing it to stdout with timestamp and storing it
let timestamp = now()
echo "[", timestamp.format("yyyy-MM-dd HH:mm:ss"), "] ChatSDK: ", message
# Store the message using the provided storage interface
if sdk.storeCallback != nil:
let storeResult = sdk.storeCallback(cstring(id), cstring(message), sdk.userData)
return storeResult == 0
return false
# Get message method for ChatSDK
proc getMessage*(sdk: ChatSDK, id: string): string =
## Gets a message using the provided storage interface
if sdk.getCallback != nil:
let messageResult = sdk.getCallback(cstring(id), sdk.userData)
if messageResult != nil:
return $messageResult
return ""
# Original standalone sendMessage for backward compatibility
proc sendMessage*(message: string) =
## Sends a message by printing it to stdout with timestamp
let timestamp = now()
echo "[", timestamp.format("yyyy-MM-dd HH:mm:ss"), "] ChatSDK: ", message
# C-compatible wrappers
proc sendMessageCString*(message: cstring): cint {.exportc, dynlib.} =
## C-compatible wrapper for standalone sendMessage
try:
sendMessage($message)
return 0 # Success
except:
return 1 # Error
proc newChatSDKC*(storeProc: StoreMessageProc, getProc: GetMessageProc, userData: pointer = nil): ptr ChatSDK {.exportc, dynlib.} =
## C-compatible wrapper to create a new ChatSDK instance
try:
let sdk = newChatSDK(storeProc, getProc, userData)
let sdkPtr = cast[ptr ChatSDK](alloc(sizeof(ChatSDK)))
sdkPtr[] = sdk
return sdkPtr
except:
return nil
proc freeChatSDKC*(sdk: ptr ChatSDK) {.exportc, dynlib.} =
## C-compatible wrapper to free ChatSDK instance
if sdk != nil:
dealloc(sdk)
proc sendMessageSDKC*(sdk: ptr ChatSDK, id: cstring, message: cstring): cint {.exportc, dynlib.} =
## C-compatible wrapper for ChatSDK sendMessage
try:
if sdk == nil:
return 1
let success = sdk[].sendMessage($id, $message)
return if success: 0 else: 1
except:
return 1
proc getMessageSDKC*(sdk: ptr ChatSDK, id: cstring): cstring {.exportc, dynlib.} =
## C-compatible wrapper for ChatSDK getMessage
try:
if sdk == nil:
return nil
let message = sdk[].getMessage($id) # Convert cstring to string for internal method
if message.len > 0:
# Allocate C string - caller must free
let cStr = cast[cstring](alloc(message.len + 1))
copyMem(cStr, cstring(message), message.len + 1)
return cStr
return nil
except:
return nil
proc freeCString*(str: cstring) {.exportc, dynlib.} =
## Free a C string allocated by the library
if str != nil:
dealloc(str)
# Export the module for C bindings
when isMainModule:
sendMessage("Test message from Nim!")
# This is just an example to get you started. A typical library package
# exports the main API in this file. Note that you cannot rename this file
# but you can remove it if you wish.