diff --git a/Makefile b/Makefile index f39a3394..48e74af4 100644 --- a/Makefile +++ b/Makefile @@ -232,6 +232,7 @@ format: $(NPH) *.nim $(NPH) codex/ $(NPH) tests/ + $(NPH) library/ clean-nph: rm -f $(NPH) @@ -242,4 +243,21 @@ print-nph-path: clean: | clean-nph +################ +## C Bindings ## +################ +.PHONY: libcodex + +STATIC ?= 0 + +libcodex: deps + rm -f build/libcodex* + +ifeq ($(STATIC), 1) + echo -e $(BUILD_MSG) "build/$@.a" && \ + $(ENV_SCRIPT) nim libcodexStatic $(NIM_PARAMS) codex.nims +else + echo -e $(BUILD_MSG) "build/$@.so" && \ + $(ENV_SCRIPT) nim libcodexDynamic $(NIM_PARAMS) codex.nims +endif endif # "variables.mk" was not included diff --git a/README.md b/README.md index 2a15051f..4249c347 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,52 @@ To get acquainted with Codex, consider: The client exposes a REST API that can be used to interact with the clients. Overview of the API can be found on [api.codex.storage](https://api.codex.storage). +## Bindings + +Codex provides a C API that can be wrapped by other languages. The bindings is located in the `library` folder. +Currently, only a Go binding is included. + +### Build the C library + +```bash +make libcodex +``` + +This produces the shared library under `build/`. + +### Run the Go example + +Build the Go example: + +```bash +go build -o codex-go examples/golang/codex.go +``` + +Export the library path: + +```bash +export LD_LIBRARY_PATH=build +``` + +Run the example: + +```bash +./codex-go +``` + +### Static vs Dynamic build + +By default, Codex builds a dynamic library (`libcodex.so`), which you can load at runtime. +If you prefer a static library (`libcodex.a`), set the `STATIC` flag: + +```bash +# Build dynamic (default) +make libcodex + +# Build static +make STATIC=1 libcodex +``` + ## Contributing and development Feel free to dive in, contributions are welcomed! Open an issue or submit PRs. diff --git a/build.nims b/build.nims index 88660321..b7bbcf8b 100644 --- a/build.nims +++ b/build.nims @@ -25,6 +25,20 @@ proc buildBinary(name: string, srcDir = "./", params = "", lang = "c") = exec(cmd) +proc buildLibrary(name: string, srcDir = "./", params = "", `type` = "static") = + if not dirExists "build": + mkDir "build" + # allow something like "nim nimbus --verbosity:0 --hints:off nimbus.nims" + var extra_params = params + if `type` == "static": + exec "nim c" & " --out:build/" & name & + ".a --threads:on --app:staticlib --opt:size --noMain --mm:refc --header --d:metrics --nimMainPrefix:libcodex --skipParentCfg:on -d:noSignalHandler " & + extra_params & " " & srcDir & name & ".nim" + else: + exec "nim c" & " --out:build/" & name & + ".so --threads:on --app:lib --opt:size --noMain --mm:refc --header --d:metrics --nimMainPrefix:libcodex --skipParentCfg:on -d:noSignalHandler -d:LeopardCmakeFlags=\"-DCMAKE_POSITION_INDEPENDENT_CODE=ON\"" & + extra_params & " " & srcDir & name & ".nim" + proc test(name: string, srcDir = "tests/", params = "", lang = "c") = buildBinary name, srcDir, params exec "build/" & name @@ -121,3 +135,11 @@ task showCoverage, "open coverage html": echo " ======== Opening HTML coverage report in browser... ======== " if findExe("open") != "": exec("open coverage/report/index.html") + +task libcodexDynamic, "Generate bindings": + let name = "libcodex" + buildLibrary name, "library/", "", "dynamic" + +task libcodextatic, "Generate bindings": + let name = "libcodex" + buildLibrary name, "library/", "", "static" diff --git a/examples/golang/README.md b/examples/golang/README.md new file mode 100644 index 00000000..30a12932 --- /dev/null +++ b/examples/golang/README.md @@ -0,0 +1,24 @@ + +## Pre-requisite + +libcodex.so is needed to be compiled and present in build folder. + +## Compilation + +From the codex root folder: + +```code +go build -o codex-go examples/golang/codex.go +``` + +## Run +From the codex root folder: + + +```code +export LD_LIBRARY_PATH=build +``` + +```code +./codex-go +``` diff --git a/examples/golang/codex.go b/examples/golang/codex.go new file mode 100644 index 00000000..aab6bc2f --- /dev/null +++ b/examples/golang/codex.go @@ -0,0 +1,296 @@ +package main + +/* + #cgo LDFLAGS: -L../../build/ -lcodex + #cgo LDFLAGS: -L../../ -Wl,-rpath,../../ + + #include "../../library/libcodex.h" + #include + #include + + void libcodexNimMain(void); + static void codex_host_init_once(void){ + static int done; + if (!__atomic_exchange_n(&done, 1, __ATOMIC_SEQ_CST)) libcodexNimMain(); + } + + extern void globalEventCallback(int ret, char* msg, size_t len, void* userData); + + typedef struct { + int ret; + char* msg; + size_t len; + } Resp; + + static void* allocResp() { + return calloc(1, sizeof(Resp)); + } + + static void freeResp(void* resp) { + if (resp != NULL) { + free(resp); + } + } + + static char* getMyCharPtr(void* resp) { + if (resp == NULL) { + return NULL; + } + Resp* m = (Resp*) resp; + return m->msg; + } + + static size_t getMyCharLen(void* resp) { + if (resp == NULL) { + return 0; + } + Resp* m = (Resp*) resp; + return m->len; + } + + static int getRet(void* resp) { + if (resp == NULL) { + return 0; + } + Resp* m = (Resp*) resp; + return m->ret; + } + + // resp must be set != NULL in case interest on retrieving data from the callback + static void callback(int ret, char* msg, size_t len, void* resp) { + if (resp != NULL) { + Resp* m = (Resp*) resp; + m->ret = ret; + m->msg = msg; + m->len = len; + } + } + + #define CODEX_CALL(call) \ + do { \ + int ret = call; \ + if (ret != 0) { \ + printf("Failed the call to: %s. Returned code: %d\n", #call, ret); \ + exit(1); \ + } \ + } while (0) + + static void* cGoCodexNew(const char* configJson, void* resp) { + void* ret = codex_new(configJson, (CodexCallback) callback, resp); + return ret; + } + + static void cGoCodexStart(void* codexCtx, void* resp) { + CODEX_CALL(codex_start(codexCtx, (CodexCallback) callback, resp)); + } + + static void cGoCodexStop(void* codexCtx, void* resp) { + CODEX_CALL(codex_stop(codexCtx, (CodexCallback) callback, resp)); + } + + static void cGoCodexDestroy(void* codexCtx, void* resp) { + CODEX_CALL(codex_destroy(codexCtx, (CodexCallback) callback, resp)); + } + + static void cGoCodexSetEventCallback(void* codexCtx) { + // The 'globalEventCallback' Go function is shared amongst all possible Codex instances. + + // Given that the 'globalEventCallback' is shared, we pass again the + // codexCtx instance but in this case is needed to pick up the correct method + // that will handle the event. + + // In other words, for every call the libcodex makes to globalEventCallback, + // the 'userData' parameter will bring the context of the node that registered + // that globalEventCallback. + + // This technique is needed because cgo only allows to export Go functions and not methods. + + codex_set_event_callback(codexCtx, (CodexCallback) globalEventCallback, codexCtx); + } + +*/ +import "C" + +import ( + "encoding/json" + "errors" + "fmt" + "log" + "os" + "os/signal" + "syscall" + "unsafe" +) + +type LogLevel string + +const ( + Trace LogLevel = "TRACE" + Debug LogLevel = "DEBUG" + Info LogLevel = "INFO" + Notice LogLevel = "NOTICE" + Warn LogLevel = "WARN" + Error LogLevel = "ERROR" + Fatal LogLevel = "FATAL" +) + +type LogFormat string + +const ( + LogFormatAuto LogFormat = "auto" + LogFormatColors LogFormat = "colors" + LogFormatNoColors LogFormat = "nocolors" + LogFormatJSON LogFormat = "json" +) + +type RepoKind string + +const ( + FS RepoKind = "fs" + SQLite RepoKind = "sqlite" + LevelDb RepoKind = "leveldb" +) + +type CodexConfig struct { + LogLevel LogLevel `json:"log-level,omitempty"` + LogFormat LogFormat `json:"log-format,omitempty"` + MetricsEnabled bool `json:"metrics,omitempty"` + MetricsAddress string `json:"metrics-address,omitempty"` + DataDir string `json:"data-dir,omitempty"` + ListenAddrs []string `json:"listen-addrs,omitempty"` + Nat string `json:"nat,omitempty"` + DiscoveryPort int `json:"disc-port,omitempty"` + NetPrivKeyFile string `json:"net-privkey,omitempty"` + BootstrapNodes []byte `json:"bootstrap-node,omitempty"` + MaxPeers int `json:"max-peers,omitempty"` + NumThreads int `json:"num-threads,omitempty"` + AgentString string `json:"agent-string,omitempty"` + RepoKind RepoKind `json:"repo-kind,omitempty"` + StorageQuota int `json:"storage-quota,omitempty"` + BlockTtl int `json:"block-ttl,omitempty"` + BlockMaintenanceInterval int `json:"block-mi,omitempty"` + BlockMaintenanceNumberOfBlocks int `json:"block-mn,omitempty"` + CacheSize int `json:"cache-size,omitempty"` + LogFile string `json:"log-file,omitempty"` +} + +type CodexNode struct { + ctx unsafe.Pointer +} + +func CodexNew(config CodexConfig) (*CodexNode, error) { + jsonConfig, err := json.Marshal(config) + if err != nil { + return nil, err + } + + var cJsonConfig = C.CString(string(jsonConfig)) + var resp = C.allocResp() + + defer C.free(unsafe.Pointer(cJsonConfig)) + defer C.freeResp(resp) + + ctx := C.cGoCodexNew(cJsonConfig, resp) + if C.getRet(resp) == C.RET_OK { + return &CodexNode{ctx: ctx}, nil + } + + errMsg := "error CodexNew: " + C.GoStringN(C.getMyCharPtr(resp), C.int(C.getMyCharLen(resp))) + return nil, errors.New(errMsg) +} + +func (self *CodexNode) CodexStart() error { + var resp = C.allocResp() + defer C.freeResp(resp) + C.cGoCodexStart(self.ctx, resp) + + if C.getRet(resp) == C.RET_OK { + return nil + } + errMsg := "error CodexStart: " + C.GoStringN(C.getMyCharPtr(resp), C.int(C.getMyCharLen(resp))) + return errors.New(errMsg) +} + +func (self *CodexNode) CodexStop() error { + var resp = C.allocResp() + defer C.freeResp(resp) + C.cGoCodexStop(self.ctx, resp) + + if C.getRet(resp) == C.RET_OK { + return nil + } + errMsg := "error CodexStop: " + C.GoStringN(C.getMyCharPtr(resp), C.int(C.getMyCharLen(resp))) + return errors.New(errMsg) +} + +func (self *CodexNode) CodexDestroy() error { + var resp = C.allocResp() + defer C.freeResp(resp) + C.cGoCodexDestroy(self.ctx, resp) + + if C.getRet(resp) == C.RET_OK { + return nil + } + errMsg := "error CodexDestroy: " + C.GoStringN(C.getMyCharPtr(resp), C.int(C.getMyCharLen(resp))) + return errors.New(errMsg) +} + +//export globalEventCallback +func globalEventCallback(callerRet C.int, msg *C.char, len C.size_t, userData unsafe.Pointer) { + // This is shared among all Golang instances + + self := CodexNode{ctx: userData} + self.MyEventCallback(callerRet, msg, len) +} + +func (self *CodexNode) MyEventCallback(callerRet C.int, msg *C.char, len C.size_t) { + fmt.Println("Event received:", C.GoStringN(msg, C.int(len))) +} + +func (self *CodexNode) CodexSetEventCallback() { + // Notice that the events for self node are handled by the 'MyEventCallback' method + C.cGoCodexSetEventCallback(self.ctx) +} + +func main() { + config := CodexConfig{ + LogLevel: Info, + } + + log.Println("Starting Codex...") + + node, err := CodexNew(config) + if err != nil { + fmt.Println("Error happened:", err.Error()) + return + } + + node.CodexSetEventCallback() + + err = node.CodexStart() + if err != nil { + fmt.Println("Error happened:", err.Error()) + return + } + + log.Println("Codex started...") + + // Wait for a SIGINT or SIGTERM signal + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) + <-ch + + log.Println("Stopping the node...") + + err = node.CodexStop() + if err != nil { + fmt.Println("Error happened:", err.Error()) + return + } + + err = node.CodexDestroy() + if err != nil { + fmt.Println("Error happened:", err.Error()) + return + } +} diff --git a/library/README.md b/library/README.md new file mode 100644 index 00000000..db6423fe --- /dev/null +++ b/library/README.md @@ -0,0 +1,37 @@ +# Codex Library + +Codex exposes a C binding that serves as a stable contract, making it straightforward to integrate Codex into other languages such as Go. + +The implementation was inspired by [nim-library-template](https://github.com/logos-co/nim-library-template) +and by the [nwaku](https://github.com/waku-org/nwaku/tree/master/library) library. + +The source code contains detailed comments to explain the threading and callback flow. +The diagram below summarizes the lifecycle: context creation, request execution, and shutdown. + +```mermaid +sequenceDiagram + autonumber + actor App as App/User + participant Go as Go Wrapper + participant C as C API (libcodex.h) + participant Ctx as CodexContext + participant Thr as Worker Thread + participant Eng as CodexServer + + App->>Go: Start + Go->>C: codex_start_node + C->>Ctx: enqueue request + C->>Ctx: fire signal + Ctx->>Thr: wake worker + Thr->>Ctx: dequeue request + Thr-->>Ctx: ACK + Ctx-->>C: forward ACK + C-->>Go: RET OK + Go->>App: Unblock + Thr->>Eng: execute (async) + Eng-->>Thr: result ready + Thr-->>Ctx: callback + Ctx-->>C: forward callback + C-->>Go: forward callback + Go-->>App: done +``` \ No newline at end of file diff --git a/library/alloc.nim b/library/alloc.nim new file mode 100644 index 00000000..1a6f118b --- /dev/null +++ b/library/alloc.nim @@ -0,0 +1,42 @@ +## Can be shared safely between threads +type SharedSeq*[T] = tuple[data: ptr UncheckedArray[T], len: int] + +proc alloc*(str: cstring): cstring = + # Byte allocation from the given address. + # There should be the corresponding manual deallocation with deallocShared ! + if str.isNil(): + var ret = cast[cstring](allocShared(1)) # Allocate memory for the null terminator + ret[0] = '\0' # Set the null terminator + return ret + + let ret = cast[cstring](allocShared(len(str) + 1)) + copyMem(ret, str, len(str) + 1) + return ret + +proc alloc*(str: string): cstring = + ## Byte allocation from the given address. + ## There should be the corresponding manual deallocation with deallocShared ! + var ret = cast[cstring](allocShared(str.len + 1)) + let s = cast[seq[char]](str) + for i in 0 ..< str.len: + ret[i] = s[i] + ret[str.len] = '\0' + return ret + +proc allocSharedSeq*[T](s: seq[T]): SharedSeq[T] = + let data = allocShared(sizeof(T) * s.len) + if s.len != 0: + copyMem(data, unsafeAddr s[0], s.len) + return (cast[ptr UncheckedArray[T]](data), s.len) + +proc deallocSharedSeq*[T](s: var SharedSeq[T]) = + deallocShared(s.data) + s.len = 0 + +proc toSeq*[T](s: SharedSeq[T]): seq[T] = + ## Creates a seq[T] from a SharedSeq[T]. No explicit dealloc is required + ## as req[T] is a GC managed type. + var ret = newSeq[T]() + for i in 0 ..< s.len: + ret.add(s.data[i]) + return ret diff --git a/library/codex_context.nim b/library/codex_context.nim new file mode 100644 index 00000000..592b0b9c --- /dev/null +++ b/library/codex_context.nim @@ -0,0 +1,198 @@ +## This file defines the Codex context and its thread flow: +## 1. Client enqueues a request and signals the Codex thread. +## 2. The Codex thread dequeues the request and sends an ack (reqReceivedSignal). +## 3. The Codex thread executes the request asynchronously. +## 4. On completion, the Codex thread invokes the client callback with the result and userData. + +{.pragma: exported, exportc, cdecl, raises: [].} +{.pragma: callback, cdecl, raises: [], gcsafe.} +{.passc: "-fPIC".} + +import std/[options, locks, atomics] +import chronicles +import chronos +import chronos/threadsync +import taskpools/channels_spsc_single +import ./ffi_types +import ./codex_thread_requests/[codex_thread_request] + +from ../codex/codex import CodexServer + +type CodexContext* = object + thread: Thread[(ptr CodexContext)] + + # This lock is only necessary while we use a SP Channel and while the signalling + # between threads assumes that there aren't concurrent requests. + # Rearchitecting the signaling + migrating to a MP Channel will allow us to receive + # requests concurrently and spare us the need of locks + lock: Lock + + # Channel to send requests to the Codex thread. + # Requests will be popped from this channel. + reqChannel: ChannelSPSCSingle[ptr CodexThreadRequest] + + # To notify the Codex thread that a request is ready + reqSignal: ThreadSignalPtr + + # To notify the client thread that the request was received. + # It is acknowledgment signal (handshake). + reqReceivedSignal: ThreadSignalPtr + + # Custom state attached by the client to a request, + # returned when its callback is invoked + userData*: pointer + + # Function called by the library to notify the client of global events + eventCallback*: pointer + + # Custom state attached by the client to the context, + # returned with every event callback + eventUserData*: pointer + + # Set to false to stop the Codex thread (during codex_destroy) + running: Atomic[bool] + +template callEventCallback(ctx: ptr CodexContext, eventName: string, body: untyped) = + ## Template used to notify the client of global events + ## Example: onConnectionChanged, onProofMissing, etc. + if isNil(ctx[].eventCallback): + error eventName & " - eventCallback is nil" + return + + foreignThreadGc: + try: + let event = body + cast[CodexCallback](ctx[].eventCallback)( + RET_OK, unsafeAddr event[0], cast[csize_t](len(event)), ctx[].eventUserData + ) + except Exception, CatchableError: + let msg = + "Exception " & eventName & " when calling 'eventCallBack': " & + getCurrentExceptionMsg() + cast[CodexCallback](ctx[].eventCallback)( + RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), ctx[].eventUserData + ) + +proc sendRequestToCodexThread*( + ctx: ptr CodexContext, + reqType: RequestType, + reqContent: pointer, + callback: CodexCallback, + userData: pointer, + timeout = InfiniteDuration, +): Result[void, string] = + ctx.lock.acquire() + + defer: + ctx.lock.release() + + let req = CodexThreadRequest.createShared(reqType, reqContent, callback, userData) + + # Send the request to the Codex thread + let sentOk = ctx.reqChannel.trySend(req) + if not sentOk: + deallocShared(req) + return err("Couldn't send a request to the codex thread: " & $req[]) + + # Notify the Codex thread that a request is available + let fireSyncRes = ctx.reqSignal.fireSync() + if fireSyncRes.isErr(): + deallocShared(req) + return err("failed fireSync: " & $fireSyncRes.error) + + if fireSyncRes.get() == false: + deallocShared(req) + return err("Couldn't fireSync in time") + + # Wait until the Codex Thread properly received the request + let res = ctx.reqReceivedSignal.waitSync(timeout) + if res.isErr(): + deallocShared(req) + return err("Couldn't receive reqReceivedSignal signal") + + ## Notice that in case of "ok", the deallocShared(req) is performed by the Codex Thread in the + ## process proc. See the 'codex_thread_request.nim' module for more details. + ok() + +proc runCodex(ctx: ptr CodexContext) {.async.} = + var codex: CodexServer + + while true: + # Wait until a request is available + await ctx.reqSignal.wait() + + # If codex_destroy was called, exit the loop + if ctx.running.load == false: + break + + var request: ptr CodexThreadRequest + + # Pop a request from the channel + let recvOk = ctx.reqChannel.tryRecv(request) + if not recvOk: + error "codex thread could not receive a request" + continue + + # Dispatch the request to be processed asynchronously + asyncSpawn CodexThreadRequest.process(request, addr codex) + + # Notify the main thread that we picked up the request + let fireRes = ctx.reqReceivedSignal.fireSync() + if fireRes.isErr(): + error "could not fireSync back to requester thread", error = fireRes.error + +proc run(ctx: ptr CodexContext) {.thread.} = + waitFor runCodex(ctx) + +proc createCodexContext*(): Result[ptr CodexContext, string] = + ## This proc is called from the main thread and it creates + ## the Codex working thread. + + # Allocates a CodexContext in shared memory (for the main thread) + var ctx = createShared(CodexContext, 1) + + # This signal is used by the main side to wake the Codex thread + # when a new request is enqueued. + ctx.reqSignal = ThreadSignalPtr.new().valueOr: + return err("couldn't create reqSignal ThreadSignalPtr") + + # Used to let the caller know that the Codex thread has + # acknowledged / picked up a request (like a handshake). + ctx.reqReceivedSignal = ThreadSignalPtr.new().valueOr: + return err("couldn't create reqReceivedSignal ThreadSignalPtr") + + # Protects shared state inside CodexContext + ctx.lock.initLock() + + # Codex thread will loop until codex_destroy is called + ctx.running.store(true) + + try: + createThread(ctx.thread, run, ctx) + except ValueError, ResourceExhaustedError: + freeShared(ctx) + return err("failed to create the Codex thread: " & getCurrentExceptionMsg()) + + return ok(ctx) + +proc destroyCodexContext*(ctx: ptr CodexContext): Result[void, string] = + # Signal the Codex thread to stop + ctx.running.store(false) + + # Wake the worker up if it's waiting + let signaledOnTime = ctx.reqSignal.fireSync().valueOr: + return err("error in destroyCodexContext: " & $error) + + if not signaledOnTime: + return err("failed to signal reqSignal on time in destroyCodexContext") + + # Wait for the thread to finish + joinThread(ctx.thread) + + # Clean up + ctx.lock.deinitLock() + ?ctx.reqSignal.close() + ?ctx.reqReceivedSignal.close() + freeShared(ctx) + + return ok() diff --git a/library/codex_thread_requests/codex_thread_request.nim b/library/codex_thread_requests/codex_thread_request.nim new file mode 100644 index 00000000..9e2c7ae1 --- /dev/null +++ b/library/codex_thread_requests/codex_thread_request.nim @@ -0,0 +1,81 @@ +## This file contains the base message request type that will be handled. +## The requests are created by the main thread and processed by +## the Codex Thread. + +import std/json +import results +import chronos +import ../ffi_types +import ./requests/node_lifecycle_request + +from ../../codex/codex import CodexServer + +type RequestType* {.pure.} = enum + LIFECYCLE + +type CodexThreadRequest* = object + reqType: RequestType + + # Request payloed + reqContent: pointer + + # Callback to notify the client thread of the result + callback: CodexCallback + + # Custom state attached by the client to the request, + # returned when its callback is invoked. + userData: pointer + +proc createShared*( + T: type CodexThreadRequest, + reqType: RequestType, + reqContent: pointer, + callback: CodexCallback, + userData: pointer, +): ptr type T = + var ret = createShared(T) + ret[].reqType = reqType + ret[].reqContent = reqContent + ret[].callback = callback + ret[].userData = userData + return ret + +proc handleRes[T: string | void]( + res: Result[T, string], request: ptr CodexThreadRequest +) = + ## Handles the Result responses, which can either be Result[string, string] or + ## Result[void, string]. + defer: + deallocShared(request) + + if res.isErr(): + foreignThreadGc: + let msg = "libcodex error: handleRes fireSyncRes error: " & $res.error + request[].callback( + RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), request[].userData + ) + return + + foreignThreadGc: + var msg: cstring = "" + when T is string: + msg = res.get().cstring() + request[].callback( + RET_OK, unsafeAddr msg[0], cast[csize_t](len(msg)), request[].userData + ) + return + +proc process*( + T: type CodexThreadRequest, request: ptr CodexThreadRequest, codex: ptr CodexServer +) {.async.} = + ## Processes the request in the Codex thread. + ## Dispatch to the appropriate request handler based on reqType. + let retFut = + case request[].reqType + of LIFECYCLE: + cast[ptr NodeLifecycleRequest](request[].reqContent).process(codex) + + handleRes(await retFut, request) + +proc `$`*(self: CodexThreadRequest): string = + return $self.reqType diff --git a/library/codex_thread_requests/requests/node_lifecycle_request.nim b/library/codex_thread_requests/requests/node_lifecycle_request.nim new file mode 100644 index 00000000..9a4c623d --- /dev/null +++ b/library/codex_thread_requests/requests/node_lifecycle_request.nim @@ -0,0 +1,165 @@ +## This file contains the lifecycle request type that will be handled. + +import std/[options, json, strutils, net, os] +import confutils/defs +import codexdht/discv5/spr +import stew/shims/parseutils +import contractabi/address +import chronos +import chronicles +import results +import confutils +import confutils/std/net +import libp2p +import json_serialization +import json_serialization/std/[options, net] +import ../../alloc +import ../../../codex/conf +import ../../../codex/utils +import ../../../codex/utils/[keyutils, fileutils] + +from ../../../codex/codex import CodexServer, new, start, stop + +type NodeLifecycleMsgType* = enum + CREATE_NODE + START_NODE + STOP_NODE + +proc readValue*[T: InputFile | InputDir | OutPath | OutDir | OutFile]( + r: var JsonReader, val: var T +) = + val = T(r.readValue(string)) + +proc readValue*(r: var JsonReader, val: var MultiAddress) = + val = MultiAddress.init(r.readValue(string)).get() + +proc readValue*(r: var JsonReader, val: var NatConfig) = + let res = NatConfig.parse(r.readValue(string)) + if res.isErr: + raise + newException(SerializationError, "Cannot parse the NAT config: " & res.error()) + val = res.get() + +proc readValue*(r: var JsonReader, val: var SignedPeerRecord) = + let res = SignedPeerRecord.parse(r.readValue(string)) + if res.isErr: + raise + newException(SerializationError, "Cannot parse the signed peer: " & res.error()) + val = res.get() + +proc readValue*(r: var JsonReader, val: var ThreadCount) = + let res = ThreadCount.parse(r.readValue(string)) + if res.isErr: + raise + newException(SerializationError, "Cannot parse the thread count: " & res.error()) + val = res.get() + +proc readValue*(r: var JsonReader, val: var NBytes) = + let res = NBytes.parse(r.readValue(string)) + if res.isErr: + raise newException(SerializationError, "Cannot parse the NBytes: " & res.error()) + val = res.get() + +proc readValue*(r: var JsonReader, val: var Duration) = + var dur: Duration + let input = r.readValue(string) + let count = parseDuration(input, dur) + if count == 0: + raise newException(SerializationError, "Cannot parse the duration: " & input) + val = dur + +proc readValue*(r: var JsonReader, val: var EthAddress) = + val = EthAddress.init(r.readValue(string)).get() + +type NodeLifecycleRequest* = object + operation: NodeLifecycleMsgType + configJson: cstring + +proc createShared*( + T: type NodeLifecycleRequest, op: NodeLifecycleMsgType, configJson: cstring = "" +): ptr type T = + var ret = createShared(T) + ret[].operation = op + ret[].configJson = configJson.alloc() + return ret + +proc destroyShared(self: ptr NodeLifecycleRequest) = + deallocShared(self[].configJson) + deallocShared(self) + +proc createCodex(configJson: cstring): Future[Result[CodexServer, string]] {.async.} = + var conf = CodexConf.load( + version = codexFullVersion, + envVarsPrefix = "codex", + cmdLine = @[], + secondarySources = proc( + config: CodexConf, sources: auto + ) {.gcsafe, raises: [ConfigurationError].} = + if configJson.len > 0: + sources.addConfigFileContent(Json, $(configJson)) + , + ) + + conf.setupLogging() + conf.setupMetrics() + + if not (checkAndCreateDataDir((conf.dataDir).string)): + # We are unable to access/create data folder or data folder's + # permissions are insecure. + return err( + "Unable to access/create data folder or data folder's permissions are insecure." + ) + + if not (checkAndCreateDataDir((conf.dataDir / "repo"))): + # We are unable to access/create data folder or data folder's + # permissions are insecure. + return err( + "Unable to access/create data folder or data folder's permissions are insecure." + ) + + debug "Repo dir initialized", dir = conf.dataDir / "repo" + + let keyPath = + if isAbsolute(conf.netPrivKeyFile): + conf.netPrivKeyFile + else: + conf.dataDir / conf.netPrivKeyFile + let privateKey = setupKey(keyPath).expect("Should setup private key!") + + let server = + try: + CodexServer.new(conf, privateKey) + except Exception as exc: + return err("Failed to start Codex: " & exc.msg) + + return ok(server) + +proc process*( + self: ptr NodeLifecycleRequest, codex: ptr CodexServer +): Future[Result[string, string]] {.async.} = + defer: + destroyShared(self) + + case self.operation + of CREATE_NODE: + codex[] = ( + await createCodex( + self.configJson # , self.appCallbacks + ) + ).valueOr: + error "CREATE_NODE failed", error = error + return err($error) + of START_NODE: + try: + await codex[].start() + except Exception as e: + error "START_NODE failed", error = e.msg + return err(e.msg) + of STOP_NODE: + try: + await codex[].stop() + except Exception as e: + error "STOP_NODE failed", error = e.msg + return err(e.msg) + + return ok("") diff --git a/library/events/json_base_event.nim b/library/events/json_base_event.nim new file mode 100644 index 00000000..743444ed --- /dev/null +++ b/library/events/json_base_event.nim @@ -0,0 +1,14 @@ +# JSON Event definition +# +# This file defines de JsonEvent type, which serves as the base +# for all event types in the library +# +# Reference specification: +# https://github.com/vacp2p/rfc/blob/master/content/docs/rfcs/36/README.md#jsonsignal-type + +type JsonEvent* = ref object of RootObj + eventType* {.requiresInit.}: string + +method `$`*(jsonEvent: JsonEvent): string {.base.} = + discard + # All events should implement this diff --git a/library/ffi_types.nim b/library/ffi_types.nim new file mode 100644 index 00000000..9882636b --- /dev/null +++ b/library/ffi_types.nim @@ -0,0 +1,35 @@ +# FFI Types and Utilities +# +# This file defines the core types and utilities for the library's foreign +# function interface (FFI), enabling interoperability with external code. + +################################################################################ +### Exported types + +type CodexCallback* = proc( + callerRet: cint, msg: ptr cchar, len: csize_t, userData: pointer +) {.cdecl, gcsafe, raises: [].} + +const RET_OK*: cint = 0 +const RET_ERR*: cint = 1 +const RET_MISSING_CALLBACK*: cint = 2 + +### End of exported types +################################################################################ + +################################################################################ +### FFI utils + +template foreignThreadGc*(body: untyped) = + when declared(setupForeignThreadGc): + setupForeignThreadGc() + + body + + when declared(tearDownForeignThreadGc): + tearDownForeignThreadGc() + +type onDone* = proc() + +### End of FFI utils +################################################################################ diff --git a/library/libcodex.h b/library/libcodex.h new file mode 100644 index 00000000..1350d83c --- /dev/null +++ b/library/libcodex.h @@ -0,0 +1,54 @@ +/** +* libcodex.h - C Interface for Example Library +* +* This header provides the public API for libcodex +* +* To see the auto-generated header by Nim, run `make libcodex` from the +* repository root. The generated file will be created at: +* nimcache/release/libcodex/libcodex.h +*/ + +#ifndef __libcodex__ +#define __libcodex__ + +#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 (*CodexCallback) (int callerRet, const char* msg, size_t len, void* userData); + +void* codex_new( + const char* configJson, + CodexCallback callback, + void* userData); + +int codex_start(void* ctx, + CodexCallback callback, + void* userData); + +int codex_stop(void* ctx, + CodexCallback callback, + void* userData); + +// Destroys an instance of a codex node created with codex_new +int codex_destroy(void* ctx, + CodexCallback callback, + void* userData); + +void codex_set_event_callback(void* ctx, + CodexCallback callback, + void* userData); + +#ifdef __cplusplus +} +#endif + +#endif /* __libcodex__ */ \ No newline at end of file diff --git a/library/libcodex.nim b/library/libcodex.nim new file mode 100644 index 00000000..b5bf21f6 --- /dev/null +++ b/library/libcodex.nim @@ -0,0 +1,165 @@ +# libcodex.nim - C-exported interface for the Codex shared library +# +# This file implements the public C API for libcodex. +# It acts as the bridge between C programs and the internal Nim implementation. +# +# This file defines: +# - Initialization logic for the Nim runtime (once per process) +# - Thread-safe exported procs callable from C +# - Callback registration and invocation for asynchronous communication + +# cdecl is C declaration calling convention. +# It’s the standard way C compilers expect functions to behave: +# 1- Caller cleans up the stack after the call +# 2- Symbol names are exported in a predictable way +# In other termes, it is a glue that makes Nim functions callable as normal C functions. +{.pragma: exported, exportc, cdecl, raises: [].} +{.pragma: callback, cdecl, raises: [], gcsafe.} + +# Ensure code is position-independent so it can be built into a shared library (.so). +# In other terms, the code that can run no matter where it’s placed in memory. +{.passc: "-fPIC".} + +when defined(linux): + # Define the canonical name for this library + {.passl: "-Wl,-soname,libcodex.so".} + +import std/[atomics] +import chronicles +import chronos +import ./codex_context +import ./codex_thread_requests/codex_thread_request +import ./codex_thread_requests/requests/node_lifecycle_request +import ./ffi_types + +template checkLibcodexParams*( + ctx: ptr CodexContext, callback: CodexCallback, userData: pointer +) = + if not isNil(ctx): + ctx[].userData = userData + + if isNil(callback): + return RET_MISSING_CALLBACK + +proc handleRequest( + ctx: ptr CodexContext, + requestType: RequestType, + content: pointer, + callback: CodexCallback, + userData: pointer, +): cint = + codex_context.sendRequestToCodexThread(ctx, requestType, content, callback, userData).isOkOr: + let msg = "libcodex error: " & $error + callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) + return RET_ERR + + return RET_OK + +# From Nim doc: +# "the C targets require you to initialize Nim's internals, which is done calling a NimMain function." +# "The name NimMain can be influenced via the --nimMainPrefix:prefix switch." +# "Use --nimMainPrefix:MyLib and the function to call is named MyLibNimMain." +proc libcodexNimMain() {.importc.} + +# Atomic flag to prevent multiple initializations +var initialized: Atomic[bool] + +if defined(android): + # Redirect chronicles to Android System logs + when compiles(defaultChroniclesStream.outputs[0].writer): + defaultChroniclesStream.outputs[0].writer = proc( + logLevel: LogLevel, msg: LogOutputStr + ) {.raises: [].} = + echo logLevel, msg + +# Initializes the Nim runtime and foreign-thread GC +proc initializeLibrary() {.exported.} = + if not initialized.exchange(true): + ## Every Nim library must call `NimMain()` once + libcodexNimMain() + when declared(setupForeignThreadGc): + setupForeignThreadGc() + when declared(nimGC_setStackBottom): + var locals {.volatile, noinit.}: pointer + locals = addr(locals) + nimGC_setStackBottom(locals) + +proc codex_new( + configJson: cstring, callback: CodexCallback, userData: pointer +): pointer {.dynlib, exportc, cdecl.} = + initializeLibrary() + + if isNil(callback): + error "Missing callback in codex_new" + return nil + + var ctx = codex_context.createCodexContext().valueOr: + let msg = "Error in createCodexContext: " & $error + callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) + return nil + + ctx.userData = userData + + let retCode = handleRequest( + ctx, + RequestType.LIFECYCLE, + NodeLifecycleRequest.createShared( + NodeLifecycleMsgType.CREATE_NODE, configJson # , appCallbacks + ), + callback, + userData, + ) + + if retCode == RET_ERR: + return nil + + return ctx + +proc codex_destroy( + ctx: ptr CodexContext, callback: COdexCallback, userData: pointer +): cint {.dynlib, exportc.} = + initializeLibrary() + checkLibcodexParams(ctx, callback, userData) + + codex_context.destroyCodexContext(ctx).isOkOr: + let msg = "libcodex error: " & $error + callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) + return RET_ERR + + ## always need to invoke the callback although we don't retrieve value to the caller + callback(RET_OK, nil, 0, userData) + + return RET_OK + +proc codex_start( + ctx: ptr CodexContext, callback: CodexCallback, userData: pointer +): cint {.dynlib, exportc.} = + initializeLibrary() + checkLibcodexParams(ctx, callback, userData) + handleRequest( + ctx, + RequestType.LIFECYCLE, + NodeLifecycleRequest.createShared(NodeLifecycleMsgType.START_NODE), + callback, + userData, + ) + +proc codex_stop( + ctx: ptr CodexContext, callback: CodexCallback, userData: pointer +): cint {.dynlib, exportc.} = + initializeLibrary() + checkLibcodexParams(ctx, callback, userData) + handleRequest( + ctx, + RequestType.LIFECYCLE, + NodeLifecycleRequest.createShared(NodeLifecycleMsgType.STOP_NODE), + callback, + userData, + ) + +proc codex_set_event_callback( + ctx: ptr CodexContext, callback: CodexCallback, userData: pointer +) {.dynlib, exportc.} = + initializeLibrary() + ctx[].eventCallback = cast[pointer](callback) + ctx[].eventUserData = userData