mirror of
https://github.com/logos-storage/logos-storage-nim.git
synced 2026-01-07 16:03:13 +00:00
Init library
This commit is contained in:
parent
be759baf4d
commit
5f547b9043
18
Makefile
18
Makefile
@ -232,6 +232,7 @@ format:
|
|||||||
$(NPH) *.nim
|
$(NPH) *.nim
|
||||||
$(NPH) codex/
|
$(NPH) codex/
|
||||||
$(NPH) tests/
|
$(NPH) tests/
|
||||||
|
$(NPH) library/
|
||||||
|
|
||||||
clean-nph:
|
clean-nph:
|
||||||
rm -f $(NPH)
|
rm -f $(NPH)
|
||||||
@ -242,4 +243,21 @@ print-nph-path:
|
|||||||
|
|
||||||
clean: | clean-nph
|
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
|
endif # "variables.mk" was not included
|
||||||
|
|||||||
46
README.md
46
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).
|
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
|
## Contributing and development
|
||||||
|
|
||||||
Feel free to dive in, contributions are welcomed! Open an issue or submit PRs.
|
Feel free to dive in, contributions are welcomed! Open an issue or submit PRs.
|
||||||
|
|||||||
22
build.nims
22
build.nims
@ -25,6 +25,20 @@ proc buildBinary(name: string, srcDir = "./", params = "", lang = "c") =
|
|||||||
|
|
||||||
exec(cmd)
|
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") =
|
proc test(name: string, srcDir = "tests/", params = "", lang = "c") =
|
||||||
buildBinary name, srcDir, params
|
buildBinary name, srcDir, params
|
||||||
exec "build/" & name
|
exec "build/" & name
|
||||||
@ -121,3 +135,11 @@ task showCoverage, "open coverage html":
|
|||||||
echo " ======== Opening HTML coverage report in browser... ======== "
|
echo " ======== Opening HTML coverage report in browser... ======== "
|
||||||
if findExe("open") != "":
|
if findExe("open") != "":
|
||||||
exec("open coverage/report/index.html")
|
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"
|
||||||
|
|||||||
24
examples/golang/README.md
Normal file
24
examples/golang/README.md
Normal file
@ -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
|
||||||
|
```
|
||||||
296
examples/golang/codex.go
Normal file
296
examples/golang/codex.go
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo LDFLAGS: -L../../build/ -lcodex
|
||||||
|
#cgo LDFLAGS: -L../../ -Wl,-rpath,../../
|
||||||
|
|
||||||
|
#include "../../library/libcodex.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
37
library/README.md
Normal file
37
library/README.md
Normal file
@ -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
|
||||||
|
```
|
||||||
42
library/alloc.nim
Normal file
42
library/alloc.nim
Normal file
@ -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
|
||||||
198
library/codex_context.nim
Normal file
198
library/codex_context.nim
Normal file
@ -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()
|
||||||
81
library/codex_thread_requests/codex_thread_request.nim
Normal file
81
library/codex_thread_requests/codex_thread_request.nim
Normal file
@ -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
|
||||||
@ -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("")
|
||||||
14
library/events/json_base_event.nim
Normal file
14
library/events/json_base_event.nim
Normal file
@ -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
|
||||||
35
library/ffi_types.nim
Normal file
35
library/ffi_types.nim
Normal file
@ -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
|
||||||
|
################################################################################
|
||||||
54
library/libcodex.h
Normal file
54
library/libcodex.h
Normal file
@ -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 <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
// 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__ */
|
||||||
165
library/libcodex.nim
Normal file
165
library/libcodex.nim
Normal file
@ -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 `<prefix>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
|
||||||
Loading…
x
Reference in New Issue
Block a user