diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..55c7570 --- /dev/null +++ b/Makefile @@ -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 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9ad3f26 --- /dev/null +++ b/README.md @@ -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 +``` \ No newline at end of file diff --git a/bindings/c-bindings/Makefile b/bindings/c-bindings/Makefile new file mode 100644 index 0000000..d8de2fd --- /dev/null +++ b/bindings/c-bindings/Makefile @@ -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 \ No newline at end of file diff --git a/bindings/c-bindings/chatsdk.h b/bindings/c-bindings/chatsdk.h new file mode 100644 index 0000000..a10187f --- /dev/null +++ b/bindings/c-bindings/chatsdk.h @@ -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 \ No newline at end of file diff --git a/bindings/c-bindings/libchatsdk.so b/bindings/c-bindings/libchatsdk.so new file mode 100755 index 0000000..13f3f8e Binary files /dev/null and b/bindings/c-bindings/libchatsdk.so differ diff --git a/bindings/go-bindings/callbacks.go b/bindings/go-bindings/callbacks.go new file mode 100644 index 0000000..fd2249a --- /dev/null +++ b/bindings/go-bindings/callbacks.go @@ -0,0 +1,56 @@ +package chatsdk + +/* +#include +*/ +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) +} diff --git a/bindings/go-bindings/chatsdk.go b/bindings/go-bindings/chatsdk.go new file mode 100644 index 0000000..33f163c --- /dev/null +++ b/bindings/go-bindings/chatsdk.go @@ -0,0 +1,169 @@ +package chatsdk + +/* +#cgo CFLAGS: -I../c-bindings +#cgo LDFLAGS: -L../c-bindings -lchatsdk +#include "chatsdk.h" +#include + +// 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 +} diff --git a/bindings/go-bindings/go.mod b/bindings/go-bindings/go.mod new file mode 100644 index 0000000..9c924cf --- /dev/null +++ b/bindings/go-bindings/go.mod @@ -0,0 +1,3 @@ +module github.com/waku-org/nim-chat-sdk/bindings/go-bindings + +go 1.24 diff --git a/chat_sdk.nimble b/chat_sdk.nimble index d9f6b71..2eca593 100644 --- a/chat_sdk.nimble +++ b/chat_sdk.nimble @@ -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" diff --git a/examples/go-app/go.mod b/examples/go-app/go.mod new file mode 100644 index 0000000..ed2205e --- /dev/null +++ b/examples/go-app/go.mod @@ -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 diff --git a/examples/go-app/main.go b/examples/go-app/main.go new file mode 100644 index 0000000..7517c46 --- /dev/null +++ b/examples/go-app/main.go @@ -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!") +} diff --git a/src/chat_sdk.nim b/src/chat_sdk.nim index b7a2480..931bea8 100644 --- a/src/chat_sdk.nim +++ b/src/chat_sdk.nim @@ -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.