diff --git a/.gitignore b/.gitignore index 5a3358e..7fe05ef 100644 --- a/.gitignore +++ b/.gitignore @@ -91,3 +91,5 @@ result # App data .registry/ .savedkeys/ + +AGENTS.md diff --git a/.gitmodules b/.gitmodules index 44e4b4a..f1cc4cf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -41,3 +41,8 @@ url = https://github.com/mratsim/constantine.git ignore = untracked branch = master +[submodule "vendor/nim-ffi"] + path = vendor/nim-ffi + url = https://github.com/logos-messaging/nim-ffi/ + ignore = untracked + branch = master diff --git a/Makefile b/Makefile index 9fad87e..a86b85f 100644 --- a/Makefile +++ b/Makefile @@ -102,6 +102,28 @@ tui bot_echo pingpong: | build-waku-librln build-waku-nat nim_chat_poc.nims echo -e $(BUILD_MSG) "build/$@" && \ $(ENV_SCRIPT) nim $@ $(NIM_PARAMS) --path:src nim_chat_poc.nims +########### +## Library ## +########### + +# Determine shared library extension based on OS +ifeq ($(shell uname -s),Darwin) + LIBCHAT_EXT := dylib +else ifeq ($(shell uname -s),Linux) + LIBCHAT_EXT := so +else + LIBCHAT_EXT := dll +endif + +LIBCHAT := build/libchat.$(LIBCHAT_EXT) + +.PHONY: libchat +libchat: | build-waku-librln build-waku-nat nim_chat_poc.nims + echo -e $(BUILD_MSG) "$(LIBCHAT)" && \ + $(ENV_SCRIPT) nim libchat $(NIM_PARAMS) --path:src nim_chat_poc.nims && \ + echo -e "\n\x1B[92mLibrary built successfully:\x1B[39m" && \ + echo " $(shell pwd)/$(LIBCHAT)" + endif diff --git a/config.nims b/config.nims new file mode 100644 index 0000000..27a5d0b --- /dev/null +++ b/config.nims @@ -0,0 +1,7 @@ +import std/os + +# all vendor subdirectories +for dir in walkDir(thisDir() / "vendor"): + if dir.kind == pcDir: + switch("path", dir.path) + switch("path", dir.path / "src") diff --git a/examples/cbindings/.clang-format b/examples/cbindings/.clang-format new file mode 100644 index 0000000..97c975d --- /dev/null +++ b/examples/cbindings/.clang-format @@ -0,0 +1,6 @@ +BasedOnStyle: LLVM +IndentWidth: 4 +TabWidth: 4 +UseTab: Never +BreakBeforeBraces: Attach +ColumnLimit: 100 diff --git a/examples/cbindings/Makefile b/examples/cbindings/Makefile new file mode 100644 index 0000000..aa26a01 --- /dev/null +++ b/examples/cbindings/Makefile @@ -0,0 +1,39 @@ +# Makefile for cbindings_chat_tui C example + +CC = gcc +CFLAGS = -Wall -Wextra -I../../library -pthread +LDFLAGS = -L../../build -lchat -lncurses -Wl,-rpath,../../build + +BUILD_DIR = ../../build +TARGET = $(BUILD_DIR)/cbindings_chat_tui +SRC = cbindings_chat_tui.c + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(SRC) + $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) + +clean: + rm -f $(TARGET) + +# Run with default settings +run: $(TARGET) + LD_LIBRARY_PATH=$(BUILD_DIR) $(TARGET) + +# Run as alice on port 60001 +run_alice: $(TARGET) + LD_LIBRARY_PATH=$(BUILD_DIR) $(TARGET) --name=Alice --port=60001 + +# Run as bob on port 60002 +run_bob: $(TARGET) + LD_LIBRARY_PATH=$(BUILD_DIR) $(TARGET) --name=Bob --port=60002 + +help: + @echo "Usage:" + @echo " make - Build cbindings_chat_tui" + @echo " make run - Build and run with defaults" + @echo " make run_alice - Build and run as Alice on port 60001" + @echo " make run_bob - Build and run as Bob on port 60002" + @echo " make clean - Remove built files" diff --git a/examples/cbindings/README.md b/examples/cbindings/README.md new file mode 100644 index 0000000..a2c42d8 --- /dev/null +++ b/examples/cbindings/README.md @@ -0,0 +1,59 @@ +# C Bindings Example - Chat TUI + +A simple terminal user interface that demonstrates how to use libchat from C. + +## Build + +1. First, build libchat from the root folder: + + ```bash + make libchat + ``` + +2. Then build the C example: + + ```bash + cd examples/cbindings + make + ``` + +## Run + +Terminal 1: + +```bash +make run_alice +# Runs as Alice on port 60001 +``` + +Terminal 2: + +```bash +make run_bob +# Runs as Bob on port 60002 +``` + +## Workflow + +1. Start the application - it automatically uses your inbox conversation +2. Type `/bundle` to get your IntroBundle JSON (will be copied to clipboard) +3. In the other terminal type `/join ` to start a conversation +4. You can send messages from one termnial to the other + +## Command Line Options + +```text +--name= Identity name (default: user) +--port= Waku port (default: random 50000-50200) +--cluster= Waku cluster ID (default: 42) +--shard= Waku shard ID (default: 2) +--peer= Static peer multiaddr to connect to +--help Show help +``` + +## Waku Configuration + +- **Cluster ID**: 42 +- **Shard ID**: 2 +- **PubSub Topic**: `/waku/2/rs/42/2` +- **Port**: Random between 50000-50200 (or specify with --port) diff --git a/examples/cbindings/cbindings_chat_tui.c b/examples/cbindings/cbindings_chat_tui.c new file mode 100644 index 0000000..9043f3e --- /dev/null +++ b/examples/cbindings/cbindings_chat_tui.c @@ -0,0 +1,738 @@ +/** + * Simple Terminal UI for libchat.c + * Commands: + * /join - Join a conversation + * /bundle - Show your intro bundle + * /quit - Exit + * - Send message to current conversation + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "libchat.h" + +// Constants +static const int LOG_PANEL_HEIGHT = 6; +static const int MSG_PANEL_HEIGHT = 12; +static const int MAX_MESSAGES = 100; +static const int MAX_LOGS = 50; +static const size_t MAX_LINE_LEN = 2048; +static const size_t MAX_INPUT_LEN = 2048; + +// Application state structures +typedef struct { + char current_convo[128]; + char inbox_id[128]; + char my_name[64]; + char my_address[128]; + void *ctx; +} ChatState; + +typedef struct { + char (*lines)[2048]; + int count; + int max; + pthread_mutex_t mutex; +} TextBuffer; + +typedef struct { + WINDOW *log_win, *msg_win, *input_win; + WINDOW *log_border, *msg_border; + SCREEN *screen; + FILE *tty_out, *tty_in; +} UI; + +typedef struct { + char buffer[2048]; + int len; + int pos; +} InputState; + +typedef struct { + ChatState chat; + TextBuffer messages; + TextBuffer logs; + UI ui; + InputState input; + FILE *log_file; + char log_filename[256]; + atomic_int running; + atomic_int needs_refresh; + atomic_int resize_pending; +} App; + +static App g_app; + +// Forward declarations +static void refresh_ui(void); +static void add_text(TextBuffer *buf, const char *text, const char *prefix); +static void handle_input(const char *input); + +////////////////////////////////////////////////////////////////////////////// +// Utility functions +////////////////////////////////////////////////////////////////////////////// + +static const char *get_timestamp(void) { + static char buf[16]; + time_t now = time(NULL); + struct tm *tm = localtime(&now); + snprintf(buf, sizeof(buf), "%02d:%02d:%02d", tm->tm_hour, tm->tm_min, tm->tm_sec); + return buf; +} + +// Simple JSON string value extractor +static int json_extract(const char *json, const char **keys, char **values, + size_t *sizes, int n) { + int found = 0; + for (int i = 0; i < n; i++) { + values[i][0] = '\0'; + // Build search pattern: "key": + char pattern[128]; + snprintf(pattern, sizeof(pattern), "\"%s\":", keys[i]); + const char *pos = strstr(json, pattern); + if (!pos) continue; + + pos += strlen(pattern); + while (*pos == ' ') pos++; // skip whitespace + if (*pos != '"') continue; // only handle string values + pos++; // skip opening quote + + const char *end = strchr(pos, '"'); + if (!end) continue; + + size_t len = end - pos; + if (len >= sizes[i]) len = sizes[i] - 1; + strncpy(values[i], pos, len); + values[i][len] = '\0'; + found++; + } + return found; +} + +static void string_to_hex(const char *str, char *hex, size_t hex_size) { + size_t len = strlen(str); + if (len * 2 + 1 > hex_size) + len = (hex_size - 1) / 2; + for (size_t i = 0; i < len; i++) { + snprintf(hex + i * 2, 3, "%02x", (unsigned char)str[i]); + } + hex[len * 2] = '\0'; +} + +static void hex_to_string(const char *hex, char *str, size_t str_size) { + size_t hex_len = strlen(hex); + size_t len = hex_len / 2; + if (len >= str_size) + len = str_size - 1; + for (size_t i = 0; i < len; i++) { + char byte[3] = {hex[i * 2], hex[i * 2 + 1], '\0'}; + char *end; + unsigned long val = strtoul(byte, &end, 16); + if (end != byte + 2) { + str[i] = '?'; // Invalid hex + } else { + str[i] = (char)val; + } + } + str[len] = '\0'; +} + +static int copy_to_clipboard(const char *data, size_t len) { + const char *cmd = NULL; +#ifdef __APPLE__ + cmd = "pbcopy"; +#else + if (system("which wl-copy >/dev/null 2>&1") == 0) + cmd = "wl-copy"; + else if (system("which xclip >/dev/null 2>&1") == 0) + cmd = "xclip -selection clipboard"; + else if (system("which xsel >/dev/null 2>&1") == 0) + cmd = "xsel --clipboard --input"; +#endif + if (!cmd) return 0; + FILE *pipe = popen(cmd, "w"); + if (!pipe) return 0; + fwrite(data, 1, len, pipe); + pclose(pipe); + return 1; +} + +////////////////////////////////////////////////////////////////////////////// +// Text buffer operations +////////////////////////////////////////////////////////////////////////////// + +static int textbuf_init(TextBuffer *buf, int max_lines) { + buf->lines = calloc(max_lines, MAX_LINE_LEN); + if (!buf->lines) return -1; + buf->count = 0; + buf->max = max_lines; + pthread_mutex_init(&buf->mutex, NULL); + return 0; +} + +static void textbuf_destroy(TextBuffer *buf) { + free(buf->lines); + buf->lines = NULL; + pthread_mutex_destroy(&buf->mutex); +} + +static void add_text(TextBuffer *buf, const char *text, const char *prefix) { + pthread_mutex_lock(&buf->mutex); + if (buf->count >= buf->max) { + memmove(buf->lines[0], buf->lines[1], (buf->max - 1) * MAX_LINE_LEN); + buf->count = buf->max - 1; + } + if (prefix) { + snprintf(buf->lines[buf->count], MAX_LINE_LEN, "[%s] %s", prefix, text); + } else { + snprintf(buf->lines[buf->count], MAX_LINE_LEN, "%s", text); + } + buf->count++; + pthread_mutex_unlock(&buf->mutex); + atomic_store(&g_app.needs_refresh, 1); +} + +static inline void add_message(const char *msg) { + add_text(&g_app.messages, msg, NULL); +} + +static inline void add_log(const char *log) { + add_text(&g_app.logs, log, get_timestamp()); +} + +////////////////////////////////////////////////////////////////////////////// +// ncurses UI +////////////////////////////////////////////////////////////////////////////// + +static void create_windows(void) { + int max_y, max_x; + getmaxyx(stdscr, max_y, max_x); + + int log_height = LOG_PANEL_HEIGHT + 2; + int msg_height = MSG_PANEL_HEIGHT + 2; + int input_height = 3; + int available = max_y - input_height; + + if (log_height + msg_height > available) { + log_height = available / 3; + msg_height = available - log_height; + } + + UI *ui = &g_app.ui; + ui->log_border = newwin(log_height, max_x, 0, 0); + ui->msg_border = newwin(msg_height, max_x, log_height, 0); + ui->log_win = derwin(ui->log_border, log_height - 2, max_x - 2, 1, 1); + ui->msg_win = derwin(ui->msg_border, msg_height - 2, max_x - 2, 1, 1); + ui->input_win = newwin(input_height, max_x, log_height + msg_height, 0); + + scrollok(ui->log_win, TRUE); + scrollok(ui->msg_win, TRUE); + keypad(ui->input_win, TRUE); + nodelay(ui->input_win, TRUE); + + if (has_colors()) { + start_color(); + use_default_colors(); + init_pair(1, COLOR_CYAN, -1); + init_pair(2, COLOR_GREEN, -1); + init_pair(3, COLOR_YELLOW, -1); + init_pair(4, COLOR_RED, -1); + init_pair(5, COLOR_MAGENTA, -1); + } +} + +static void destroy_windows(void) { + UI *ui = &g_app.ui; + if (ui->log_win) delwin(ui->log_win); + if (ui->msg_win) delwin(ui->msg_win); + if (ui->input_win) delwin(ui->input_win); + if (ui->log_border) delwin(ui->log_border); + if (ui->msg_border) delwin(ui->msg_border); + memset(ui, 0, sizeof(*ui) - sizeof(ui->screen) - sizeof(ui->tty_out) - sizeof(ui->tty_in)); +} + +static void draw_borders(void) { + UI *ui = &g_app.ui; + ChatState *chat = &g_app.chat; + + wattron(ui->log_border, COLOR_PAIR(3) | A_DIM); + box(ui->log_border, 0, 0); + mvwprintw(ui->log_border, 0, 2, " Logs "); + wattroff(ui->log_border, COLOR_PAIR(3) | A_DIM); + + wattron(ui->msg_border, COLOR_PAIR(1) | A_BOLD); + box(ui->msg_border, 0, 0); + if (chat->current_convo[0]) { + mvwprintw(ui->msg_border, 0, 2, " Messages [%s] [%s] ", chat->my_name, chat->current_convo); + } else { + mvwprintw(ui->msg_border, 0, 2, " Messages [%s] [no conversation] ", chat->my_name); + } + wattroff(ui->msg_border, COLOR_PAIR(1) | A_BOLD); + + wattron(ui->input_win, COLOR_PAIR(2) | A_BOLD); + box(ui->input_win, 0, 0); + mvwprintw(ui->input_win, 0, 2, " Input "); + wattroff(ui->input_win, COLOR_PAIR(2) | A_BOLD); + + wnoutrefresh(ui->log_border); + wnoutrefresh(ui->msg_border); +} + +static void draw_textbuf(WINDOW *win, TextBuffer *buf) { + werase(win); + pthread_mutex_lock(&buf->mutex); + + int max_y = getmaxy(win); + int max_x = getmaxx(win); + int total_lines = 0; + int *lines_per = alloca(buf->count * sizeof(int)); + + for (int i = 0; i < buf->count; i++) { + int len = (int)strlen(buf->lines[i]); + lines_per[i] = len == 0 ? 1 : (len + max_x - 1) / max_x; + total_lines += lines_per[i]; + } + + int skip = total_lines > max_y ? total_lines - max_y : 0; + int start = 0, skipped = 0; + while (start < buf->count && skipped + lines_per[start] <= skip) { + skipped += lines_per[start++]; + } + + int row = 0; + for (int i = start; i < buf->count && row < max_y; i++) { + wmove(win, row, 0); + if (buf == &g_app.logs) wattron(win, A_DIM); + wprintw(win, "%s", buf->lines[i]); + if (buf == &g_app.logs) wattroff(win, A_DIM); + row += lines_per[i]; + } + + pthread_mutex_unlock(&buf->mutex); + wnoutrefresh(win); +} + +static void draw_input(void) { + UI *ui = &g_app.ui; + InputState *inp = &g_app.input; + int max_x = getmaxx(ui->input_win); + + mvwhline(ui->input_win, 1, 1, ' ', max_x - 2); + wattron(ui->input_win, COLOR_PAIR(2) | A_BOLD); + mvwprintw(ui->input_win, 1, 1, "> "); + wattroff(ui->input_win, COLOR_PAIR(2) | A_BOLD); + + int available = max_x - 5; + int display_start = inp->pos > available - 1 ? inp->pos - available + 1 : 0; + mvwprintw(ui->input_win, 1, 3, "%.*s", available, inp->buffer + display_start); + wmove(ui->input_win, 1, 3 + inp->pos - display_start); + wnoutrefresh(ui->input_win); +} + +static void refresh_ui(void) { + if (!atomic_exchange(&g_app.needs_refresh, 0)) return; + + if (atomic_exchange(&g_app.resize_pending, 0)) { + endwin(); + refresh(); + destroy_windows(); + create_windows(); + } + + draw_borders(); + draw_textbuf(g_app.ui.log_win, &g_app.logs); + draw_textbuf(g_app.ui.msg_win, &g_app.messages); + draw_input(); + doupdate(); +} + +////////////////////////////////////////////////////////////////////////////// +// Signal handling (async-signal-safe) +////////////////////////////////////////////////////////////////////////////// + +static void handle_sigint(int sig) { + (void)sig; + atomic_store(&g_app.running, 0); +} + +static void handle_sigwinch(int sig) { + (void)sig; + atomic_store(&g_app.resize_pending, 1); + atomic_store(&g_app.needs_refresh, 1); +} + +////////////////////////////////////////////////////////////////////////////// +// FFI Callbacks +////////////////////////////////////////////////////////////////////////////// + +static void general_callback(int ret, const char *msg, size_t len, void *userData) { + (void)userData; + char buf[256]; + if (ret == RET_OK) { + snprintf(buf, sizeof(buf), "OK%s%.*s", len > 0 ? ": " : "", (int)(len > 60 ? 60 : len), msg); + } else { + snprintf(buf, sizeof(buf), "ERR: %.*s", (int)(len > 60 ? 60 : len), msg); + } + add_log(buf); +} + +static void event_callback(int ret, const char *msg, size_t len, void *userData) { + (void)userData; + (void)ret; + (void)len; + + char event_type[32] = {0}; + char convo_id[128] = {0}; + char content[2048] = {0}; + + const char *keys[] = {"eventType", "conversationId", "content"}; + char *values[] = {event_type, convo_id, content}; + size_t sizes[] = {sizeof(event_type), sizeof(convo_id), sizeof(content)}; + json_extract(msg, keys, values, sizes, 3); + + if (strcmp(event_type, "new_message") == 0) { + char decoded[2048], buf[2048]; + hex_to_string(content, decoded, sizeof(decoded)); + snprintf(buf, sizeof(buf), "<- %s", decoded); + add_message(buf); + } else if (strcmp(event_type, "new_conversation") == 0) { + strncpy(g_app.chat.current_convo, convo_id, sizeof(g_app.chat.current_convo) - 1); + char buf[256]; + snprintf(buf, sizeof(buf), "* New conversation: %.32s...", g_app.chat.current_convo); + add_message(buf); + } else if (strcmp(event_type, "delivery_ack") == 0) { + add_log("Delivery acknowledged"); + } + + char buf[256]; + snprintf(buf, sizeof(buf), "EVT: %.70s%s", msg, len > 70 ? "..." : ""); + add_log(buf); +} + +static void bundle_callback(int ret, const char *msg, size_t len, void *userData) { + (void)userData; + if (ret == RET_OK && len > 0) { + char buf[2048]; + snprintf(buf, sizeof(buf), "%.*s", (int)(len < sizeof(buf) - 1 ? len : sizeof(buf) - 1), msg); + + if (copy_to_clipboard(msg, len)) { + add_message("Your IntroBundle (copied to clipboard):"); + add_log("Bundle copied to clipboard"); + } else { + add_message("Your IntroBundle:"); + } + add_message(""); + add_message(buf); + add_message(""); + } else { + char buf[256]; + snprintf(buf, sizeof(buf), "Failed to get bundle: %.*s", (int)len, msg); + add_message(buf); + } +} + +static void identity_callback(int ret, const char *msg, size_t len, void *userData) { + (void)userData; (void)len; + if (ret == RET_OK) { + const char *keys[] = {"name", "address"}; + char *values[] = {g_app.chat.my_name, g_app.chat.my_address}; + size_t sizes[] = {sizeof(g_app.chat.my_name), sizeof(g_app.chat.my_address)}; + json_extract(msg, keys, values, sizes, 2); + + char buf[256]; + snprintf(buf, sizeof(buf), "Identity: %s (%.24s...)", g_app.chat.my_name, g_app.chat.my_address); + add_log(buf); + } +} + +static void inbox_callback(int ret, const char *msg, size_t len, void *userData) { + (void)userData; + if (ret == RET_OK && len > 0) { + snprintf(g_app.chat.inbox_id, sizeof(g_app.chat.inbox_id), "%.*s", (int)len, msg); + char buf[256]; + snprintf(buf, sizeof(buf), "Inbox: %.24s...", g_app.chat.inbox_id); + add_log(buf); + } +} + +////////////////////////////////////////////////////////////////////////////// +// Command handling +////////////////////////////////////////////////////////////////////////////// + +static void cmd_join(const char *args) { + if (!args || !*args) { + add_message("Usage: /join "); + return; + } + char hex_msg[256]; + string_to_hex("Hello!", hex_msg, sizeof(hex_msg)); + chat_new_private_conversation(g_app.chat.ctx, general_callback, NULL, args, hex_msg); + add_message("* Creating conversation..."); +} + +static void cmd_send(const char *message) { + if (!g_app.chat.current_convo[0]) { + add_message("No active conversation. Use /join or receive an invite."); + return; + } + char hex_msg[4096]; + string_to_hex(message, hex_msg, sizeof(hex_msg)); + chat_send_message(g_app.chat.ctx, general_callback, NULL, g_app.chat.current_convo, hex_msg); + + char buf[2048]; + snprintf(buf, sizeof(buf), "-> You: %s", message); + add_message(buf); +} + +static void handle_input(const char *input) { + if (!input || !*input) return; + + if (input[0] != '/') { + cmd_send(input); + return; + } + + if (strncmp(input, "/quit", 5) == 0 || strncmp(input, "/q", 2) == 0) { + atomic_store(&g_app.running, 0); + } else if (strncmp(input, "/join ", 6) == 0) { + cmd_join(input + 6); + } else if (strncmp(input, "/bundle", 7) == 0) { + chat_create_intro_bundle(g_app.chat.ctx, bundle_callback, NULL); + } else if (strncmp(input, "/help", 5) == 0) { + add_message("Commands:"); + add_message(" /join - Join conversation with IntroBundle"); + add_message(" /bundle - Show your IntroBundle"); + add_message(" /quit - Exit"); + add_message(" - Send message"); + } else { + char buf[256]; + snprintf(buf, sizeof(buf), "Unknown command: %s", input); + add_message(buf); + } +} + +////////////////////////////////////////////////////////////////////////////// +// Input processing +////////////////////////////////////////////////////////////////////////////// + +static void process_input_char(int ch) { + InputState *inp = &g_app.input; + + switch (ch) { + case '\n': + case KEY_ENTER: + if (inp->len > 0) { + inp->buffer[inp->len] = '\0'; + handle_input(inp->buffer); + inp->len = inp->pos = 0; + inp->buffer[0] = '\0'; + } + break; + case KEY_BACKSPACE: + case 127: + case 8: + if (inp->pos > 0) { + memmove(inp->buffer + inp->pos - 1, inp->buffer + inp->pos, inp->len - inp->pos + 1); + inp->pos--; + inp->len--; + } + break; + case KEY_DC: + if (inp->pos < inp->len) { + memmove(inp->buffer + inp->pos, inp->buffer + inp->pos + 1, inp->len - inp->pos); + inp->len--; + } + break; + case KEY_LEFT: + if (inp->pos > 0) inp->pos--; + break; + case KEY_RIGHT: + if (inp->pos < inp->len) inp->pos++; + break; + default: + if (ch >= 32 && ch < 127 && inp->len < (int)MAX_INPUT_LEN - 1) { + memmove(inp->buffer + inp->pos + 1, inp->buffer + inp->pos, inp->len - inp->pos + 1); + inp->buffer[inp->pos++] = ch; + inp->len++; + } + break; + } + atomic_store(&g_app.needs_refresh, 1); +} + +////////////////////////////////////////////////////////////////////////////// +// Initialization and cleanup +////////////////////////////////////////////////////////////////////////////// + +static int init_logging(const char *name) { + time_t now = time(NULL); + snprintf(g_app.log_filename, sizeof(g_app.log_filename), "chat_tui_%s_%ld.log", name, (long)now); + + g_app.log_file = fopen(g_app.log_filename, "w"); + if (!g_app.log_file) { + g_app.log_file = fopen("/dev/null", "w"); + } + + g_app.ui.tty_out = fopen("/dev/tty", "w"); + g_app.ui.tty_in = fopen("/dev/tty", "r"); + if (!g_app.ui.tty_out || !g_app.ui.tty_in) { + fprintf(stderr, "Error: Could not open /dev/tty\n"); + return -1; + } + + fflush(stdout); + fflush(stderr); + dup2(fileno(g_app.log_file), STDOUT_FILENO); + dup2(fileno(g_app.log_file), STDERR_FILENO); + return 0; +} + +static int init_ui(void) { + g_app.ui.screen = newterm(NULL, g_app.ui.tty_out, g_app.ui.tty_in); + if (!g_app.ui.screen) return -1; + + set_term(g_app.ui.screen); + cbreak(); + noecho(); + curs_set(1); + if (has_colors()) { + start_color(); + use_default_colors(); + } + create_windows(); + return 0; +} + +static void cleanup(void) { + if (g_app.chat.ctx) { + chat_stop(g_app.chat.ctx, general_callback, NULL); + chat_destroy(g_app.chat.ctx, general_callback, NULL); + } + + destroy_windows(); + if (g_app.ui.screen) { + endwin(); + delscreen(g_app.ui.screen); + } + + if (g_app.ui.tty_out) fclose(g_app.ui.tty_out); + if (g_app.ui.tty_in) fclose(g_app.ui.tty_in); + if (g_app.log_file) fclose(g_app.log_file); + + textbuf_destroy(&g_app.messages); + textbuf_destroy(&g_app.logs); + + FILE *tty = fopen("/dev/tty", "w"); + if (tty) { + fprintf(tty, "Goodbye! (Library logs saved to %s)\n", g_app.log_filename); + fclose(tty); + } +} + +////////////////////////////////////////////////////////////////////////////// +// Main +////////////////////////////////////////////////////////////////////////////// + +int main(int argc, char *argv[]) { + const char *name = "user"; + int port = 0, cluster_id = 42, shard_id = 2; + const char *peer = NULL; + + for (int i = 1; i < argc; i++) { + if (strncmp(argv[i], "--name=", 7) == 0) name = argv[i] + 7; + else if (strncmp(argv[i], "--port=", 7) == 0) port = atoi(argv[i] + 7); + else if (strncmp(argv[i], "--cluster=", 10) == 0) cluster_id = atoi(argv[i] + 10); + else if (strncmp(argv[i], "--shard=", 8) == 0) shard_id = atoi(argv[i] + 8); + else if (strncmp(argv[i], "--peer=", 7) == 0) peer = argv[i] + 7; + else if (strcmp(argv[i], "--help") == 0) { + printf("Usage: %s [options]\n", argv[0]); + printf(" --name= Your display name\n"); + printf(" --port= Listen port (0 for random)\n"); + printf(" --cluster= Cluster ID (default: 42)\n"); + printf(" --shard= Shard ID (default: 2)\n"); + printf(" --peer= Static peer multiaddr\n"); + return 0; + } + } + + // Initialize application state + memset(&g_app, 0, sizeof(g_app)); + strncpy(g_app.chat.my_name, name, sizeof(g_app.chat.my_name) - 1); + atomic_store(&g_app.running, 1); + + if (textbuf_init(&g_app.messages, MAX_MESSAGES) < 0 || + textbuf_init(&g_app.logs, MAX_LOGS) < 0) { + fprintf(stderr, "Failed to allocate buffers\n"); + return 1; + } + + if (init_logging(name) < 0) { + textbuf_destroy(&g_app.messages); + textbuf_destroy(&g_app.logs); + return 1; + } + + // Build config and create chat context + char config[2048]; + if (peer) { + snprintf(config, sizeof(config), + "{\"name\":\"%s\",\"port\":%d,\"clusterId\":%d,\"shardId\":%d,\"staticPeer\":\"%s\"}", + name, port, cluster_id, shard_id, peer); + } else { + snprintf(config, sizeof(config), + "{\"name\":\"%s\",\"port\":%d,\"clusterId\":%d,\"shardId\":%d}", + name, port, cluster_id, shard_id); + } + + g_app.chat.ctx = chat_new(config, general_callback, NULL); + if (!g_app.chat.ctx) { + fprintf(g_app.log_file, "Failed to create chat context\n"); + cleanup(); + return 1; + } + + set_event_callback(g_app.chat.ctx, event_callback, NULL); + + if (init_ui() < 0) { + fprintf(g_app.log_file, "Failed to initialize ncurses\n"); + cleanup(); + return 1; + } + + signal(SIGINT, handle_sigint); + signal(SIGWINCH, handle_sigwinch); + + add_log("Starting client..."); + chat_start(g_app.chat.ctx, general_callback, NULL); + chat_get_identity(g_app.chat.ctx, identity_callback, NULL); + chat_get_default_inbox_id(g_app.chat.ctx, inbox_callback, NULL); + + add_message("Welcome to Chat TUI!"); + add_message("Type /help for commands, /quit to exit"); + add_message(""); + + atomic_store(&g_app.needs_refresh, 1); + refresh_ui(); + + // Main loop + while (atomic_load(&g_app.running)) { + int ch; + while ((ch = wgetch(g_app.ui.input_win)) != ERR) { + process_input_char(ch); + } + refresh_ui(); + usleep(10000); + } + + add_log("Shutting down..."); + cleanup(); + return 0; +} \ No newline at end of file diff --git a/library/api/client_api.nim b/library/api/client_api.nim new file mode 100644 index 0000000..0ff2bf2 --- /dev/null +++ b/library/api/client_api.nim @@ -0,0 +1,156 @@ +## Client API - FFI bindings for Client lifecycle and operations +## Uses the {.ffi.} pragma for async request handling + +import std/json +import chronicles +import chronos +import ffi + +import ../../src/chat +import ../../src/chat/proto_types +import ../../src/chat/delivery/waku_client +import ../../src/chat/identity +import ../utils + +logScope: + topics = "chat ffi client" + +################################################# +# Client Creation Request (for chat_new) +################################################# + +type ChatCallbacks* = object + onNewMessage*: MessageCallback + onNewConversation*: NewConvoCallback + onDeliveryAck*: DeliveryAckCallback + +proc createChatClient( + configJson: cstring, chatCallbacks: ChatCallbacks +): Future[Result[Client, string]] {.async.} = + try: + let config = parseJson($configJson) + + # Parse identity name + let name = config.getOrDefault("name").getStr("anonymous") + + # Parse Waku configuration or use defaults + var wakuCfg = DefaultConfig() + + if config.hasKey("port"): + wakuCfg.port = config["port"].getInt().uint16 + + if config.hasKey("clusterId"): + wakuCfg.clusterId = config["clusterId"].getInt().uint16 + + if config.hasKey("shardId"): + wakuCfg.shardId = @[config["shardId"].getInt().uint16] + + if config.hasKey("staticPeers"): + wakuCfg.staticPeers = @[] + for peer in config["staticPeers"]: + wakuCfg.staticPeers.add(peer.getStr()) + + # Create identity + let identity = createIdentity(name) + + # Create Waku client + let wakuClient = initWakuClient(wakuCfg) + + # Create Chat client + let client = newClient(wakuClient, identity) + + # Register event handlers + client.onNewMessage(chatCallbacks.onNewMessage) + client.onNewConversation(chatCallbacks.onNewConversation) + client.onDeliveryAck(chatCallbacks.onDeliveryAck) + + notice "Chat client created", name = name + return ok(client) + except CatchableError as e: + return err("failed to create client: " & e.msg) + +registerReqFFI(CreateClientRequest, ctx: ptr FFIContext[Client]): + proc( + configJson: cstring, chatCallbacks: ChatCallbacks + ): Future[Result[string, string]] {.async.} = + ctx[].myLib[] = (await createChatClient(configJson, chatCallbacks)).valueOr: + error "CreateClientRequest failed", error = error + return err($error) + return ok("") + +################################################# +# Client Lifecycle Operations +################################################# + +proc chat_start( + ctx: ptr FFIContext[Client], + callback: FFICallBack, + userData: pointer +) {.ffi.} = + try: + await ctx[].myLib[].start() + return ok("") + except CatchableError as e: + error "chat_start failed", error = e.msg + return err("failed to start client: " & e.msg) + +proc chat_stop( + ctx: ptr FFIContext[Client], + callback: FFICallBack, + userData: pointer +) {.ffi.} = + try: + await ctx[].myLib[].stop() + return ok("") + except CatchableError as e: + error "chat_stop failed", error = e.msg + return err("failed to stop client: " & e.msg) + +################################################# +# Client Info Operations +################################################# + +proc chat_get_id( + ctx: ptr FFIContext[Client], + callback: FFICallBack, + userData: pointer +) {.ffi.} = + ## Get the client's identifier + let clientId = ctx[].myLib[].getId() + return ok(clientId) + +proc chat_get_default_inbox_id( + ctx: ptr FFIContext[Client], + callback: FFICallBack, + userData: pointer +) {.ffi.} = + ## Get the default inbox conversation ID + let inboxId = ctx[].myLib[].defaultInboxConversationId() + return ok(inboxId) + +################################################# +# Conversation List Operations +################################################# + +proc chat_list_conversations( + ctx: ptr FFIContext[Client], + callback: FFICallBack, + userData: pointer +) {.ffi.} = + ## List all conversations as JSON array + let convos = ctx[].myLib[].listConversations() + var convoList = newJArray() + for convo in convos: + convoList.add(%*{"id": convo.id()}) + return ok($convoList) + +proc chat_get_conversation( + ctx: ptr FFIContext[Client], + callback: FFICallBack, + userData: pointer, + convoId: cstring +) {.ffi.} = + ## Get a specific conversation by ID + let convo = ctx[].myLib[].getConversation($convoId) + return ok($(%*{"id": convo.id()})) + diff --git a/library/api/conversation_api.nim b/library/api/conversation_api.nim new file mode 100644 index 0000000..3d8b153 --- /dev/null +++ b/library/api/conversation_api.nim @@ -0,0 +1,78 @@ +## Conversation API - FFI bindings for conversation operations +## Uses the {.ffi.} pragma for async request handling + +import std/[json, options] +import chronicles +import chronos +import ffi +import stew/byteutils + +import ../../src/chat +import ../../src/chat/proto_types +import ../utils + +logScope: + topics = "chat ffi conversation" + +################################################# +# Private Conversation Operations +################################################# + +proc chat_new_private_conversation( + ctx: ptr FFIContext[Client], + callback: FFICallBack, + userData: pointer, + introBundleJson: cstring, + contentHex: cstring +) {.ffi.} = + ## Create a new private conversation with the given IntroBundle + ## introBundleJson: JSON string with {"ident": "hex...", "ephemeral": "hex..."} + ## contentHex: Initial message content as hex-encoded string + try: + let bundleJson = parseJson($introBundleJson) + + # Parse IntroBundle from JSON + let identBytes = hexToSeqByte(bundleJson["ident"].getStr()) + let ephemeralBytes = hexToSeqByte(bundleJson["ephemeral"].getStr()) + + let introBundle = IntroBundle( + ident: identBytes, + ephemeral: ephemeralBytes + ) + + # Convert hex content to bytes + let content = hexToSeqByte($contentHex) + + # Create the conversation + let errOpt = await ctx[].myLib[].newPrivateConversation(introBundle, content) + if errOpt.isSome(): + return err("failed to create conversation: " & $errOpt.get()) + + return ok("") + except CatchableError as e: + error "chat_new_private_conversation failed", error = e.msg + return err("failed to create private conversation: " & e.msg) + +################################################# +# Message Operations +################################################# + +proc chat_send_message( + ctx: ptr FFIContext[Client], + callback: FFICallBack, + userData: pointer, + convoId: cstring, + contentHex: cstring +) {.ffi.} = + ## Send a message to a conversation + ## convoId: Conversation ID string + ## contentHex: Message content as hex-encoded string + try: + let convo = ctx[].myLib[].getConversation($convoId) + let content = hexToSeqByte($contentHex) + + let msgId = await convo.sendMessage(content) + return ok(msgId) + except CatchableError as e: + error "chat_send_message failed", error = e.msg + return err("failed to send message: " & e.msg) diff --git a/library/api/identity_api.nim b/library/api/identity_api.nim new file mode 100644 index 0000000..bb903d8 --- /dev/null +++ b/library/api/identity_api.nim @@ -0,0 +1,54 @@ +## Identity API - FFI bindings for identity operations +## Uses the {.ffi.} pragma for async request handling + +import std/json +import chronicles +import chronos +import ffi +import stew/byteutils + +import ../../src/chat +import ../../src/chat/crypto +import ../../src/chat/proto_types +import ../utils + +logScope: + topics = "chat ffi identity" + +################################################# +# Identity Operations +################################################# + +proc chat_get_identity( + ctx: ptr FFIContext[Client], + callback: FFICallBack, + userData: pointer +) {.ffi.} = + ## Get the client identity + ## Returns JSON string: {"name": "...", "address": "...", "pubkey": "hex..."} + let ident = ctx[].myLib[].identity() + let identJson = %*{ + "name": ident.getName(), + "address": ident.getAddr(), + "pubkey": ident.getPubkey().toHex() + } + return ok($identJson) + +################################################# +# IntroBundle Operations +################################################# + +proc chat_create_intro_bundle( + ctx: ptr FFIContext[Client], + callback: FFICallBack, + userData: pointer +) {.ffi.} = + ## Create an IntroBundle for initiating private conversations + ## Returns JSON string: {"ident": "hex...", "ephemeral": "hex..."} + let bundle = ctx[].myLib[].createIntroBundle() + let bundleJson = %*{ + "ident": bundle.ident.toHex(), + "ephemeral": bundle.ephemeral.toHex() + } + return ok($bundleJson) + diff --git a/library/declare_lib.nim b/library/declare_lib.nim new file mode 100644 index 0000000..ea950cb --- /dev/null +++ b/library/declare_lib.nim @@ -0,0 +1,13 @@ +import ffi +import ../src/chat/client + +declareLibrary("chat") + +proc set_event_callback( + ctx: ptr FFIContext[Client], + callback: FFICallBack, + userData: pointer +) {.dynlib, exportc, cdecl.} = + ctx[].eventCallback = cast[pointer](callback) + ctx[].eventUserData = userData + diff --git a/library/libchat.h b/library/libchat.h new file mode 100644 index 0000000..a2bf02a --- /dev/null +++ b/library/libchat.h @@ -0,0 +1,107 @@ +// Generated manually +#ifndef __libchat__ +#define __libchat__ + +#include +#include + +// The possible returned values for the functions that return int +#define RET_OK 0 +#define RET_ERR 1 +#define RET_MISSING_CALLBACK 2 + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void (*FFICallBack)(int callerRet, const char *msg, size_t len, + void *userData); + +////////////////////////////////////////////////////////////////////////////// +// Client Lifecycle +////////////////////////////////////////////////////////////////////////////// + +// Creates a new instance of the chat client. +// Sets up the chat client from the given configuration. +// Returns a pointer to the Context needed by the rest of the API functions. +// configJson: JSON object with fields: +// - "name": string - identity name (default: "anonymous") +// - "port": int - Waku port (optional) +// - "clusterId": int - Waku cluster ID (optional) +// - "shardId": int - Waku shard ID (optional) +// - "staticPeers": array of strings - static peer multiaddrs (optional) +void *chat_new(const char *configJson, FFICallBack callback, void *userData); + +// Start the chat client and begin listening for messages +int chat_start(void *ctx, FFICallBack callback, void *userData); + +// Stop the chat client +int chat_stop(void *ctx, FFICallBack callback, void *userData); + +// Destroys an instance of a chat client created with chat_new +int chat_destroy(void *ctx, FFICallBack callback, void *userData); + +// Sets a callback that will be invoked whenever an event occurs. +// Events are JSON objects with "eventType" field: +// - "new_message": +// {"eventType":"new_message","conversationId":"...","messageId":"...","content":"hex...","timestamp":...} +// - "new_conversation": +// {"eventType":"new_conversation","conversationId":"...","conversationType":"private"} +// - "delivery_ack": +// {"eventType":"delivery_ack","conversationId":"...","messageId":"..."} +void set_event_callback(void *ctx, FFICallBack callback, void *userData); + +////////////////////////////////////////////////////////////////////////////// +// Client Info +////////////////////////////////////////////////////////////////////////////// + +// Get the client's identifier +int chat_get_id(void *ctx, FFICallBack callback, void *userData); + +// Get the default inbox conversation ID +int chat_get_default_inbox_id(void *ctx, FFICallBack callback, void *userData); + +////////////////////////////////////////////////////////////////////////////// +// Conversation Operations +////////////////////////////////////////////////////////////////////////////// + +// List all conversations as JSON array +// Returns: JSON array of objects with "id" field +int chat_list_conversations(void *ctx, FFICallBack callback, void *userData); + +// Get a specific conversation by ID +// Returns: JSON object with "id" field +int chat_get_conversation(void *ctx, FFICallBack callback, void *userData, + const char *convoId); + +// Create a new private conversation with the given IntroBundle +// introBundleJson: JSON string with {"ident": "hex...", "ephemeral": "hex..."} +// contentHex: Initial message content as hex-encoded string +int chat_new_private_conversation(void *ctx, FFICallBack callback, + void *userData, const char *introBundleJson, + const char *contentHex); + +// Send a message to a conversation +// convoId: Conversation ID string +// contentHex: Message content as hex-encoded string +// Returns: Message ID on success +int chat_send_message(void *ctx, FFICallBack callback, void *userData, + const char *convoId, const char *contentHex); + +////////////////////////////////////////////////////////////////////////////// +// Identity Operations +////////////////////////////////////////////////////////////////////////////// + +// Get the client identity +// Returns JSON: {"name": "...", "address": "...", "pubkey": "hex..."} +int chat_get_identity(void *ctx, FFICallBack callback, void *userData); + +// Create an IntroBundle for initiating private conversations +// Returns JSON: {"ident": "hex...", "ephemeral": "hex..."} +int chat_create_intro_bundle(void *ctx, FFICallBack callback, void *userData); + +#ifdef __cplusplus +} +#endif + +#endif /* __libchat__ */ diff --git a/library/libchat.nim b/library/libchat.nim new file mode 100644 index 0000000..93844b7 --- /dev/null +++ b/library/libchat.nim @@ -0,0 +1,98 @@ +## libchat - C bindings for the Chat SDK +## Main entry point for the shared library +## +## This library exposes the Chat SDK functionality through a C-compatible FFI interface. +## It uses nim-ffi for thread-safe async request handling. + +import std/[json, options] +import chronicles, chronos, ffi +import stew/byteutils + +import + ../src/chat/client, + ../src/chat/conversations, + ../src/chat/identity, + ../src/chat/delivery/waku_client, + ../src/chat/proto_types, + ./declare_lib, + ./utils + +logScope: + topics = "chat ffi" + +################################################################################ +## Include different APIs, i.e. all procs with {.ffi.} pragma +include + ./api/client_api, + ./api/conversation_api, + ./api/identity_api + +################################################################################ + +proc chat_new( + configJson: cstring, callback: FFICallBack, userData: pointer +): pointer {.dynlib, exportc, cdecl.} = + initializeLibrary() + + ## Creates a new instance of the Chat Client. + if isNil(callback): + echo "error: missing callback in chat_new" + return nil + + ## Create the Chat thread that will keep waiting for req from the main thread. + var ctx = ffi.createFFIContext[Client]().valueOr: + let msg = "Error in createFFIContext: " & $error + callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) + return nil + + ctx.userData = userData + + proc onNewMessage(ctx: ptr FFIContext[Client]): MessageCallback = + return proc(conversation: Conversation, msg: ReceivedMessage): Future[void] {.async.} = + callEventCallback(ctx, "onNewMessage"): + $newJsonMessageEvent( + conversation.id(), + "", + msg.content.toHex(), + msg.timestamp + ) + + proc onNewConversation(ctx: ptr FFIContext[Client]): NewConvoCallback = + return proc(conversation: Conversation): Future[void] {.async.} = + callEventCallback(ctx, "onNewConversation"): + $newJsonConversationEvent(conversation.id(), "private") + + proc onDeliveryAck(ctx: ptr FFIContext[Client]): DeliveryAckCallback = + return proc(conversation: Conversation, msgId: MessageId): Future[void] {.async.} = + callEventCallback(ctx, "onDeliveryAck"): + $newJsonDeliveryAckEvent(conversation.id(), msgId) + + let chatCallbacks = ChatCallbacks( + onNewMessage: onNewMessage(ctx), + onNewConversation: onNewConversation(ctx), + onDeliveryAck: onDeliveryAck(ctx), + ) + + ffi.sendRequestToFFIThread( + ctx, CreateClientRequest.ffiNewReq(callback, userData, configJson, chatCallbacks) + ).isOkOr: + let msg = "error in sendRequestToFFIThread: " & $error + callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) + return nil + + return ctx + +proc chat_destroy( + ctx: ptr FFIContext[Client], callback: FFICallBack, userData: pointer +): cint {.dynlib, exportc, cdecl.} = + initializeLibrary() + checkParams(ctx, callback, userData) + + ffi.destroyFFIContext(ctx).isOkOr: + let msg = "libchat error: " & $error + callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) + return RET_ERR + + callback(RET_OK, nil, 0, userData) + + return RET_OK diff --git a/library/nim.cfg b/library/nim.cfg new file mode 100644 index 0000000..3dc6221 --- /dev/null +++ b/library/nim.cfg @@ -0,0 +1 @@ +path = "../" diff --git a/library/utils.nim b/library/utils.nim new file mode 100644 index 0000000..5c342f7 --- /dev/null +++ b/library/utils.nim @@ -0,0 +1,114 @@ +## Utility functions for C-bindings +## Provides C-string helpers and JSON event serialization + +import std/json +import ffi + +# Re-export common FFI types +export ffi + +################################################# +# C-String Helpers +################################################# + +proc toCString*(s: string): cstring = + ## Convert Nim string to C string (caller must manage memory) + result = s.cstring + +proc fromCString*(cs: cstring): string = + ## Convert C string to Nim string (makes a copy) + if cs.isNil: + result = "" + else: + result = $cs + +proc toBytes*(data: ptr byte, len: csize_t): seq[byte] = + ## Convert C byte array to Nim seq[byte] + if data.isNil or len == 0: + result = @[] + else: + result = newSeq[byte](len) + copyMem(addr result[0], data, len) + +################################################# +# JSON Event Types +################################################# + +type + JsonEventType* = enum + EventNewMessage = "new_message" + EventNewConversation = "new_conversation" + EventDeliveryAck = "delivery_ack" + EventError = "error" + +type + JsonMessageEvent* = object + eventType*: string + conversationId*: string + messageId*: string + content*: string + timestamp*: int64 + + JsonConversationEvent* = object + eventType*: string + conversationId*: string + conversationType*: string + + JsonDeliveryAckEvent* = object + eventType*: string + conversationId*: string + messageId*: string + + JsonErrorEvent* = object + eventType*: string + error*: string + +################################################# +# JSON Event Constructors +################################################# + +proc newJsonMessageEvent*(convoId, msgId, content: string, timestamp: int64): JsonMessageEvent = + result = JsonMessageEvent( + eventType: $EventNewMessage, + conversationId: convoId, + messageId: msgId, + content: content, + timestamp: timestamp + ) + +proc newJsonConversationEvent*(convoId, convoType: string): JsonConversationEvent = + result = JsonConversationEvent( + eventType: $EventNewConversation, + conversationId: convoId, + conversationType: convoType + ) + +proc newJsonDeliveryAckEvent*(convoId, msgId: string): JsonDeliveryAckEvent = + result = JsonDeliveryAckEvent( + eventType: $EventDeliveryAck, + conversationId: convoId, + messageId: msgId + ) + +proc newJsonErrorEvent*(error: string): JsonErrorEvent = + result = JsonErrorEvent( + eventType: $EventError, + error: error + ) + +################################################# +# JSON Serialization +################################################# + +proc `$`*(event: JsonMessageEvent): string = + $(%*event) + +proc `$`*(event: JsonConversationEvent): string = + $(%*event) + +proc `$`*(event: JsonDeliveryAckEvent): string = + $(%*event) + +proc `$`*(event: JsonErrorEvent): string = + $(%*event) + diff --git a/nim_chat_poc.nimble b/nim_chat_poc.nimble index cc5537e..3d63a0a 100644 --- a/nim_chat_poc.nimble +++ b/nim_chat_poc.nimble @@ -21,7 +21,8 @@ requires "nim >= 2.2.4", "regex", "web3", "https://github.com/jazzz/nim-sds#exports", - "waku" + "waku", + "ffi" proc buildBinary(name: string, srcDir = "./", params = "", lang = "c") = if not dirExists "build": @@ -33,7 +34,25 @@ proc buildBinary(name: string, srcDir = "./", params = "", lang = "c") = exec "nim " & lang & " --out:build/" & name & " --mm:refc " & extra_params & " " & srcDir & name & ".nim" - + +proc buildLibrary(name: string, srcDir = "library/", params = "", lang = "c") = + ## Build a shared library (.so on Linux, .dylib on macOS, .dll on Windows) + if not dirExists "build": + mkDir "build" + + # Determine library extension based on OS + let libExt = when defined(macosx): "dylib" + elif defined(windows): "dll" + else: "so" + + var extra_params = params + for i in 2 ..< paramCount(): + extra_params &= " " & paramStr(i) + + exec "nim " & lang & " --app:lib --out:build/lib" & name & "." & libExt & + " --mm:refc --nimMainPrefix:lib" & name & " " & extra_params & " " & + srcDir & "lib" & name & ".nim" + proc test(name: string, params = "-d:chronicles_log_level=DEBUG", lang = "c") = buildBinary name, "tests/", params exec "build/" & name @@ -56,3 +75,7 @@ task bot_echo, "Build the EchoBot example": task pingpong, "Build the Pingpong example": let name = "pingpong" buildBinary name, "examples/", "-d:chronicles_log_level='INFO' -d:chronicles_disabled_topics='waku node' " + +task libchat, "Build the Chat SDK shared library (C bindings)": + buildLibrary "chat", "library/", + "-d:chronicles_log_level='INFO' -d:chronicles_enabled=on --path:src --path:vendor/nim-ffi"