diff --git a/codex/bridge.go b/codex/bridge.go new file mode 100644 index 0000000..625ab86 --- /dev/null +++ b/codex/bridge.go @@ -0,0 +1,159 @@ +package codex + +/* + #cgo LDFLAGS: -L../vendor/nim-codex/build/ -lcodex + #cgo LDFLAGS: -L../vendor/nim-codex/ -Wl,-rpath,../vendor/nim-codex/build + + #include "../vendor/nim-codex/library/libcodex.h" + #include + + typedef struct { + int ret; + char* msg; + size_t len; + uintptr_t h; + } Resp; + + static void* allocResp(uintptr_t h) { + Resp* r = (Resp*)calloc(1, sizeof(Resp)); + r->h = h; + return r; + } + + static void freeResp(void* resp) { + if (resp != NULL) { + free(resp); + } + } + + static int getRet(void* resp) { + if (resp == NULL) { + return 0; + } + Resp* m = (Resp*) resp; + return m->ret; + } +*/ +import "C" +import ( + "errors" + "fmt" + "runtime/cgo" + "sync" + "unsafe" +) + +// bridgeCtx is used for managing the C-Go bridge calls. +// It contains a wait group for synchronizing the calls, +// a cgo.Handle for passing context to the C code, +// a response pointer for receiving data from the C code, +// and fields for storing the result and error of the call. +type bridgeCtx struct { + wg *sync.WaitGroup + h cgo.Handle + resp unsafe.Pointer + result string + err error + + // Callback used for receiving progress updates during upload/download. + // + // For the upload, the bytes parameter indicates the number of bytes uploaded. + // If the chunk size is superior or equal to the blocksize (passed in init function), + // the callback will be called when a block is put in the store. + // Otherwise, it will be called when a chunk is pushed into the stream. + // + // For the download, the bytes is the size of the chunk received, and the chunk + // is the actual chunk of data received. + onProgress func(bytes int, chunk []byte) +} + +// newBridgeCtx creates a new bridge context for managing C-Go calls. +// The bridge context is initialized with a wait group and a cgo.Handle. +func newBridgeCtx() *bridgeCtx { + bridge := &bridgeCtx{} + bridge.wg = &sync.WaitGroup{} + bridge.wg.Add(1) + bridge.h = cgo.NewHandle(bridge) + bridge.resp = C.allocResp(C.uintptr_t(uintptr(bridge.h))) + return bridge +} + +// callError creates an error message for a failed C-Go call. +func (b *bridgeCtx) callError(name string) error { + return fmt.Errorf("failed the call to %s returned code %d", name, C.getRet(b.resp)) +} + +// free releases the resources associated with the bridge context, +// including the cgo.Handle and the response pointer. +func (b *bridgeCtx) free() { + if b.h > 0 { + b.h.Delete() + b.h = 0 + } + + if b.resp != nil { + C.freeResp(b.resp) + b.resp = nil + } +} + +// callback is the function called by the C code to communicate back to Go. +// It handles progress updates, successful completions, and errors. +// The function uses the response pointer to retrieve the bridge context +// and update its state accordingly. +// +//export callback +func callback(ret C.int, msg *C.char, len C.size_t, resp unsafe.Pointer) { + if resp == nil { + return + } + + m := (*C.Resp)(resp) + m.ret = ret + m.msg = msg + m.len = len + + if m.h == 0 { + return + } + + h := cgo.Handle(m.h) + if h == 0 { + return + } + + if v, ok := h.Value().(*bridgeCtx); ok { + switch ret { + case C.RET_PROGRESS: + if v.onProgress == nil { + return + } + if msg != nil { + chunk := C.GoBytes(unsafe.Pointer(msg), C.int(len)) + v.onProgress(int(C.int(len)), chunk) + } else { + v.onProgress(int(C.int(len)), nil) + } + case C.RET_OK: + retMsg := C.GoStringN(msg, C.int(len)) + v.result = retMsg + v.err = nil + if v.wg != nil { + v.wg.Done() + } + case C.RET_ERR: + retMsg := C.GoStringN(msg, C.int(len)) + v.err = errors.New(retMsg) + if v.wg != nil { + v.wg.Done() + } + } + } +} + +// wait waits for the bridge context to complete its operation. +// It returns the result and error of the operation. +func (b *bridgeCtx) wait() (string, error) { + b.wg.Wait() + return b.result, b.err +} diff --git a/codex/bridge.h b/codex/bridge.h new file mode 100644 index 0000000..c2809fe --- /dev/null +++ b/codex/bridge.h @@ -0,0 +1,5 @@ +#pragma once + +#include "../vendor/nim-codex/library/libcodex.h" + +extern void callback(int ret, char* msg, size_t len, void* resp); \ No newline at end of file diff --git a/codex/codex.go b/codex/codex.go new file mode 100644 index 0000000..ead436e --- /dev/null +++ b/codex/codex.go @@ -0,0 +1,224 @@ +package codex + +/* + #cgo LDFLAGS: -L../vendor/nim-codex/build/ -lcodex + #cgo LDFLAGS: -L../vendor/nim-codex/ -Wl,-rpath,../vendor/nim-codex/ + + #include "bridge.h" + #include + + void libcodexNimMain(void); + + static void codex_host_init_once(void){ + static int done; + if (!__atomic_exchange_n(&done, 1, __ATOMIC_SEQ_CST)) libcodexNimMain(); + } + + // resp must be set != NULL in case interest on retrieving data from the callback + void callback(int ret, char* msg, size_t len, void* resp); + + static void* cGoCodexNew(const char* configJson, void* resp) { + void* ret = codex_new(configJson, (CodexCallback) callback, resp); + return ret; + } + + static int cGoCodexStart(void* codexCtx, void* resp) { + return codex_start(codexCtx, (CodexCallback) callback, resp); + } + + static int cGoCodexStop(void* codexCtx, void* resp) { + return codex_stop(codexCtx, (CodexCallback) callback, resp); + } + + static int cGoCodexDestroy(void* codexCtx, void* resp) { + return codex_destroy(codexCtx, (CodexCallback) callback, resp); + } + + static int cGoCodexVersion(void* codexCtx, void* resp) { + return codex_version(codexCtx, (CodexCallback) callback, resp); + } + + static int cGoCodexRevision(void* codexCtx, void* resp) { + return codex_revision(codexCtx, (CodexCallback) callback, resp); + } + + static int cGoCodexRepo(void* codexCtx, void* resp) { + return codex_repo(codexCtx, (CodexCallback) callback, resp); + } + + static int cGoCodexSpr(void* codexCtx, void* resp) { + return codex_spr(codexCtx, (CodexCallback) callback, resp); + } +*/ +import "C" +import ( + "encoding/json" + "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 { + 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 +} + +// CodexNew creates a new Codex node with the provided configuration. +// The node is not started automatically; you need to call CodexStart +// to start it. +// It returns a Codex node that can be used to interact +// with the Codex network. +func CodexNew(config CodexConfig) (*CodexNode, error) { + bridge := newBridgeCtx() + defer bridge.free() + + jsonConfig, err := json.Marshal(config) + + if err != nil { + return nil, err + } + + cJsonConfig := C.CString(string(jsonConfig)) + defer C.free(unsafe.Pointer(cJsonConfig)) + + ctx := C.cGoCodexNew(cJsonConfig, bridge.resp) + + if _, err := bridge.wait(); err != nil { + return nil, bridge.err + } + + return &CodexNode{ctx: ctx}, bridge.err +} + +// Start starts the Codex node. +// TODO waits for the node to be fully started, +// by looking into the logs. +func (node CodexNode) Start() error { + bridge := newBridgeCtx() + defer bridge.free() + + if C.cGoCodexStart(node.ctx, bridge.resp) != C.RET_OK { + return bridge.callError("cGoCodexStart") + } + + _, err := bridge.wait() + + return err +} + +// StartAsync is the asynchronous version of Start. +func (node CodexNode) StartAsync(onDone func(error)) { + go func() { + err := node.Start() + onDone(err) + }() +} + +// Stop stops the Codex node. +func (node CodexNode) Stop() error { + bridge := newBridgeCtx() + + if C.cGoCodexStop(node.ctx, bridge.resp) != C.RET_OK { + return bridge.callError("cGoCodexStop") + } + + _, err := bridge.wait() + return err +} + +// Destroy destroys the Codex node, freeing all resources. +// The node must be stopped before calling this method. +func (node CodexNode) Destroy() error { + bridge := newBridgeCtx() + + if C.cGoCodexDestroy(node.ctx, bridge.resp) != C.RET_OK { + return bridge.callError("cGoCodexDestroy") + } + + _, err := bridge.wait() + return err +} + +// Version returns the version of the Codex node. +func (node CodexNode) Version() (string, error) { + bridge := newBridgeCtx() + defer bridge.free() + + if C.cGoCodexVersion(node.ctx, bridge.resp) != C.RET_OK { + return "", bridge.callError("cGoCodexVersion") + } + + return bridge.wait() +} + +// Revision returns the revision of the Codex node. +func (node CodexNode) Revision() (string, error) { + bridge := newBridgeCtx() + defer bridge.free() + + if C.cGoCodexRevision(node.ctx, bridge.resp) != C.RET_OK { + return "", bridge.callError("cGoCodexRevision") + } + + return bridge.wait() +} + +// Repo returns the path of the data dir folder. +func (node CodexNode) Repo() (string, error) { + bridge := newBridgeCtx() + defer bridge.free() + + if C.cGoCodexRepo(node.ctx, bridge.resp) != C.RET_OK { + return "", bridge.callError("cGoCodexRepo") + } + + return bridge.wait() +} diff --git a/codex/codex_test.go b/codex/codex_test.go new file mode 100644 index 0000000..aac3dd3 --- /dev/null +++ b/codex/codex_test.go @@ -0,0 +1,59 @@ +package codex + +import "testing" + +func TestCodexVersion(t *testing.T) { + node, err := CodexNew(CodexConfig{ + DataDir: t.TempDir(), + LogFormat: LogFormatNoColors, + }) + if err != nil { + t.Fatalf("Failed to create Codex node: %v", err) + } + defer node.Destroy() + + version, err := node.Version() + if err != nil { + t.Fatalf("Failed to get Codex version: %v", err) + } + if version == "" { + t.Fatal("Codex version is empty") + } + + t.Logf("Codex version: %s", version) +} + +func TestCodexRevision(t *testing.T) { + node, err := CodexNew(CodexConfig{ + DataDir: t.TempDir(), + LogFormat: LogFormatNoColors, + }) + if err != nil { + t.Fatalf("Failed to create Codex node: %v", err) + } + defer node.Destroy() + + revision, err := node.Revision() + if err != nil { + t.Fatalf("Failed to get Codex revision: %v", err) + } + if revision == "" { + t.Fatal("Codex revision is empty") + } + + t.Logf("Codex revision: %s", revision) +} + +func TestCodexRepo(t *testing.T) { + node := newCodexNode(t) + + repo, err := node.Repo() + if err != nil { + t.Fatalf("Failed to get Codex repo: %v", err) + } + if repo == "" { + t.Fatal("Codex repo is empty") + } + + t.Logf("Codex repo: %s", repo) +} diff --git a/codex/debug.go b/codex/debug.go new file mode 100644 index 0000000..4c96241 --- /dev/null +++ b/codex/debug.go @@ -0,0 +1,52 @@ +package codex + +/* + #include "bridge.h" + + static int cGoCodexDebug(void* codexCtx, void* resp) { + return codex_debug(codexCtx, (CodexCallback) callback, resp); + } +*/ +import "C" +import "encoding/json" + +type Node struct { + NodeId string `json:"nodeId"` + PeerId string `json:"peerId"` + Record string `json:"record"` + Address *string `json:"address"` + Seen bool `json:"seen"` +} + +type RoutingTable struct { + LocalNode Node `json:"localNode"` + Nodes []Node `json:"nodes"` +} + +type DebugInfo struct { + ID string `json:"id"` // Peer ID + Addrs []string `json:"addrs"` // Peer info addresses + Spr string `json:"spr"` // Signed Peer Record + AnnounceAddresses []string `json:"announceAddresses"` + PeersTable RoutingTable `json:"table"` +} + +// Debug retrieves debugging information from the Codex node. +func (node CodexNode) Debug() (DebugInfo, error) { + var info DebugInfo + + bridge := newBridgeCtx() + defer bridge.free() + + if C.cGoCodexDebug(node.ctx, bridge.resp) != C.RET_OK { + return info, bridge.callError("cGoCodexDebug") + } + + value, err := bridge.wait() + if err != nil { + return info, err + } + + err = json.Unmarshal([]byte(value), &info) + return info, err +} diff --git a/codex/debug_test.go b/codex/debug_test.go new file mode 100644 index 0000000..c5dd0b6 --- /dev/null +++ b/codex/debug_test.go @@ -0,0 +1,23 @@ +package codex + +import ( + "testing" +) + +func TestDebug(t *testing.T) { + codex := newCodexNode(t) + + info, err := codex.Debug() + if err != nil { + t.Fatalf("Debug call failed: %v", err) + } + if info.ID == "" { + t.Error("Debug info ID is empty") + } + if info.Spr == "" { + t.Error("Debug info Spr is empty") + } + if len(info.AnnounceAddresses) == 0 { + t.Error("Debug info AnnounceAddresses is empty") + } +} diff --git a/codex/testutil.go b/codex/testutil.go new file mode 100644 index 0000000..14af0aa --- /dev/null +++ b/codex/testutil.go @@ -0,0 +1,30 @@ +package codex + +import "testing" + +func newCodexNode(t *testing.T) *CodexNode { + node, err := CodexNew(CodexConfig{ + DataDir: t.TempDir(), + LogFormat: LogFormatNoColors, + }) + if err != nil { + t.Fatalf("Failed to create Codex node: %v", err) + } + + err = node.Start() + if err != nil { + t.Fatalf("Failed to start Codex node: %v", err) + } + + t.Cleanup(func() { + if err := node.Stop(); err != nil { + t.Logf("cleanup codex: %v", err) + } + + if err := node.Destroy(); err != nil { + t.Logf("cleanup codex: %v", err) + } + }) + + return node +}