From 965fd68785ab894708b05bb2b8bcef5de4d2ccf0 Mon Sep 17 00:00:00 2001 From: Ivan FB Date: Sun, 31 May 2026 11:03:15 +0200 Subject: [PATCH] feat(codegen): Go bindings support struct/seq/Option params The Go generator previously emitted a `// SKIPPED` stub for any proc with a struct, sequence or optional parameter, leaving Echo/Complex/Schedule uncallable. Now that the native ABI carries those as flat C-POD structs, the Go side can marshal them: emit an idiomatic Go struct per {.ffi.} type plus a `toC()` that builds the matching `C.` (C.CString for strings, a C array for seqs, present-flags for options, recursively for nested structs) and returns cleanup funcs run via defer once the call returns. The native path deep-copies every argument, so releasing the C buffers immediately is safe. The C bridge already accepted struct-by-value params via the pass-through type mapping; only the Go-side conversion and the `allSupported` gate needed work. Bare seq/Option *top-level* params (not wrapped in a struct) remain skipped, as the native ABI does not expose them either. The generated package is now self-contained: the native `.h` is emitted beside the `.go`, and the cgo directives use ${SRCDIR} so the header and the staged library resolve without extra env vars. genbindings_go runs gofmt to finalize column alignment. Co-Authored-By: Claude Opus 4.8 --- examples/timer/go_bindings/go.mod | 3 + examples/timer/go_bindings/my_timer.go | 452 +++++++++++++++++++++++++ examples/timer/go_bindings/my_timer.h | 116 +++++++ ffi.nimble | 4 + ffi/codegen/go.nim | 235 +++++++++++-- 5 files changed, 782 insertions(+), 28 deletions(-) create mode 100644 examples/timer/go_bindings/go.mod create mode 100644 examples/timer/go_bindings/my_timer.go create mode 100644 examples/timer/go_bindings/my_timer.h diff --git a/examples/timer/go_bindings/go.mod b/examples/timer/go_bindings/go.mod new file mode 100644 index 0000000..fabcf24 --- /dev/null +++ b/examples/timer/go_bindings/go.mod @@ -0,0 +1,3 @@ +module my_timer + +go 1.21 diff --git a/examples/timer/go_bindings/my_timer.go b/examples/timer/go_bindings/my_timer.go new file mode 100644 index 0000000..43d8486 --- /dev/null +++ b/examples/timer/go_bindings/my_timer.go @@ -0,0 +1,452 @@ +// Code generated by nim-ffi Go codegen. DO NOT EDIT. +package my_timer + +/* +#cgo CFLAGS: -I${SRCDIR} +#cgo LDFLAGS: -L${SRCDIR} -lmy_timer -Wl,-rpath,${SRCDIR} +#include "my_timer.h" +#include +#include +#include + +extern void my_timerGoEvent(int ret, char* msg, size_t len, void* userData); + +typedef struct { + int ret; char* msg; size_t len; int done; + pthread_mutex_t mu; pthread_cond_t cv; +} My_timerResp; + +static My_timerResp* my_timerRespNew() { + My_timerResp* r = (My_timerResp*)calloc(1, sizeof(My_timerResp)); + pthread_mutex_init(&r->mu, NULL); pthread_cond_init(&r->cv, NULL); + return r; +} +static void my_timerRespFree(My_timerResp* r) { + if (!r) return; + if (r->msg) free(r->msg); + pthread_mutex_destroy(&r->mu); pthread_cond_destroy(&r->cv); free(r); +} +static int my_timerRespRet(My_timerResp* r) { return r->ret; } +static char* my_timerRespMsg(My_timerResp* r) { return r->msg; } +static size_t my_timerRespLen(My_timerResp* r) { return r->len; } + +static void my_timerRespCb(int ret, const char* msg, size_t len, void* ud) { + My_timerResp* r = (My_timerResp*)ud; + pthread_mutex_lock(&r->mu); + r->ret = ret; + // Native ABI: (msg, len) is the raw result (RET_OK) or error (RET_ERR). + // Copy it so it survives past the callback. + char* e = (char*)malloc(len + 1); if (e) { memcpy(e, msg, len); e[len] = 0; } + r->msg = e; r->len = len; + r->done = 1; pthread_cond_signal(&r->cv); pthread_mutex_unlock(&r->mu); +} +static void my_timerRespWait(My_timerResp* r) { + pthread_mutex_lock(&r->mu); + while (!r->done) pthread_cond_wait(&r->cv, &r->mu); + pthread_mutex_unlock(&r->mu); +} + +static void* my_timerCall_my_timer_create(TimerConfig config, My_timerResp* r) { + void* ctx = my_timer_create(config, my_timerRespCb, r); + my_timerRespWait(r); + return ctx; +} +static int my_timerCall_my_timer_echo(void* ctx, EchoRequest req, My_timerResp* r) { + int rc = my_timer_echo(ctx, my_timerRespCb, r, req); + if (rc == RET_OK) my_timerRespWait(r); + return rc; +} +static int my_timerCall_my_timer_version(void* ctx, My_timerResp* r) { + int rc = my_timer_version(ctx, my_timerRespCb, r); + if (rc == RET_OK) my_timerRespWait(r); + return rc; +} +static int my_timerCall_my_timer_complex(void* ctx, ComplexRequest req, My_timerResp* r) { + int rc = my_timer_complex(ctx, my_timerRespCb, r, req); + if (rc == RET_OK) my_timerRespWait(r); + return rc; +} +static int my_timerCall_my_timer_schedule(void* ctx, JobSpec job, RetryPolicy retry, ScheduleConfig schedule, My_timerResp* r) { + int rc = my_timer_schedule(ctx, my_timerRespCb, r, job, retry, schedule); + if (rc == RET_OK) my_timerRespWait(r); + return rc; +} +static int my_timerCall_my_timer_destroy(void* ctx) { return my_timer_destroy(ctx); } +static uint64_t my_timerRegisterEvents(void* ctx) { return my_timer_add_event_listener(ctx, "", (FFICallBack)my_timerGoEvent, ctx); } +*/ +import "C" + +import ( + "errors" + "sync" + "unsafe" +) + +// TimerConfig mirrors the {.ffi.} type of the same name. +type TimerConfig struct { + Name string +} + +// toC marshals TimerConfig into its C-POD form, returning cleanup funcs to run after the call. +func (v TimerConfig) toC() (C.TimerConfig, []func()) { + var c C.TimerConfig + var frees []func() + cs_name := C.CString(v.Name) + frees = append(frees, func() { C.free(unsafe.Pointer(cs_name)) }) + c.name = cs_name + return c, frees +} + +// EchoRequest mirrors the {.ffi.} type of the same name. +type EchoRequest struct { + Message string + DelayMs int64 +} + +// toC marshals EchoRequest into its C-POD form, returning cleanup funcs to run after the call. +func (v EchoRequest) toC() (C.EchoRequest, []func()) { + var c C.EchoRequest + var frees []func() + cs_message := C.CString(v.Message) + frees = append(frees, func() { C.free(unsafe.Pointer(cs_message)) }) + c.message = cs_message + c.delayMs = C.int64_t(v.DelayMs) + return c, frees +} + +// EchoResponse mirrors the {.ffi.} type of the same name. +type EchoResponse struct { + Echoed string + TimerName string +} + +// toC marshals EchoResponse into its C-POD form, returning cleanup funcs to run after the call. +func (v EchoResponse) toC() (C.EchoResponse, []func()) { + var c C.EchoResponse + var frees []func() + cs_echoed := C.CString(v.Echoed) + frees = append(frees, func() { C.free(unsafe.Pointer(cs_echoed)) }) + c.echoed = cs_echoed + cs_timerName := C.CString(v.TimerName) + frees = append(frees, func() { C.free(unsafe.Pointer(cs_timerName)) }) + c.timerName = cs_timerName + return c, frees +} + +// ComplexRequest mirrors the {.ffi.} type of the same name. +type ComplexRequest struct { + Messages []EchoRequest + Tags []string + Note *string + Retries *int64 +} + +// toC marshals ComplexRequest into its C-POD form, returning cleanup funcs to run after the call. +func (v ComplexRequest) toC() (C.ComplexRequest, []func()) { + var c C.ComplexRequest + var frees []func() + if n_messages := len(v.Messages); n_messages > 0 { + arr_messages := C.malloc(C.size_t(n_messages) * C.size_t(unsafe.Sizeof(C.EchoRequest{}))) + sl_messages := unsafe.Slice((*C.EchoRequest)(arr_messages), n_messages) + for i := 0; i < n_messages; i++ { + cf_messages_e, ff_messages_e := (v.Messages[i]).toC() + frees = append(frees, ff_messages_e...) + sl_messages[i] = cf_messages_e + } + c.messages = (*C.EchoRequest)(arr_messages) + c.messages_len = C.size_t(n_messages) + a_messages := arr_messages + frees = append(frees, func() { C.free(a_messages) }) + } + if n_tags := len(v.Tags); n_tags > 0 { + arr_tags := C.malloc(C.size_t(n_tags) * C.size_t(unsafe.Sizeof((*C.char)(nil)))) + sl_tags := unsafe.Slice((**C.char)(arr_tags), n_tags) + for i := 0; i < n_tags; i++ { + cs_tags_e := C.CString(v.Tags[i]) + frees = append(frees, func() { C.free(unsafe.Pointer(cs_tags_e)) }) + sl_tags[i] = cs_tags_e + } + c.tags = (**C.char)(arr_tags) + c.tags_len = C.size_t(n_tags) + a_tags := arr_tags + frees = append(frees, func() { C.free(a_tags) }) + } + if v.Note != nil { + c.note_present = 1 + cs_note_o := C.CString(*v.Note) + frees = append(frees, func() { C.free(unsafe.Pointer(cs_note_o)) }) + c.note = cs_note_o + } + if v.Retries != nil { + c.retries_present = 1 + c.retries = C.int64_t(*v.Retries) + } + return c, frees +} + +// ComplexResponse mirrors the {.ffi.} type of the same name. +type ComplexResponse struct { + Summary string + ItemCount int64 + HasNote bool +} + +// toC marshals ComplexResponse into its C-POD form, returning cleanup funcs to run after the call. +func (v ComplexResponse) toC() (C.ComplexResponse, []func()) { + var c C.ComplexResponse + var frees []func() + cs_summary := C.CString(v.Summary) + frees = append(frees, func() { C.free(unsafe.Pointer(cs_summary)) }) + c.summary = cs_summary + c.itemCount = C.int64_t(v.ItemCount) + if v.HasNote { + c.hasNote = 1 + } else { + c.hasNote = 0 + } + return c, frees +} + +// EchoEvent mirrors the {.ffi.} type of the same name. +type EchoEvent struct { + Message string + EchoCount int64 +} + +// toC marshals EchoEvent into its C-POD form, returning cleanup funcs to run after the call. +func (v EchoEvent) toC() (C.EchoEvent, []func()) { + var c C.EchoEvent + var frees []func() + cs_message := C.CString(v.Message) + frees = append(frees, func() { C.free(unsafe.Pointer(cs_message)) }) + c.message = cs_message + c.echoCount = C.int64_t(v.EchoCount) + return c, frees +} + +// JobSpec mirrors the {.ffi.} type of the same name. +type JobSpec struct { + Name string + Payload []string + Priority int64 +} + +// toC marshals JobSpec into its C-POD form, returning cleanup funcs to run after the call. +func (v JobSpec) toC() (C.JobSpec, []func()) { + var c C.JobSpec + var frees []func() + cs_name := C.CString(v.Name) + frees = append(frees, func() { C.free(unsafe.Pointer(cs_name)) }) + c.name = cs_name + if n_payload := len(v.Payload); n_payload > 0 { + arr_payload := C.malloc(C.size_t(n_payload) * C.size_t(unsafe.Sizeof((*C.char)(nil)))) + sl_payload := unsafe.Slice((**C.char)(arr_payload), n_payload) + for i := 0; i < n_payload; i++ { + cs_payload_e := C.CString(v.Payload[i]) + frees = append(frees, func() { C.free(unsafe.Pointer(cs_payload_e)) }) + sl_payload[i] = cs_payload_e + } + c.payload = (**C.char)(arr_payload) + c.payload_len = C.size_t(n_payload) + a_payload := arr_payload + frees = append(frees, func() { C.free(a_payload) }) + } + c.priority = C.int64_t(v.Priority) + return c, frees +} + +// RetryPolicy mirrors the {.ffi.} type of the same name. +type RetryPolicy struct { + MaxAttempts int64 + BackoffMs int64 + RetryOn []string +} + +// toC marshals RetryPolicy into its C-POD form, returning cleanup funcs to run after the call. +func (v RetryPolicy) toC() (C.RetryPolicy, []func()) { + var c C.RetryPolicy + var frees []func() + c.maxAttempts = C.int64_t(v.MaxAttempts) + c.backoffMs = C.int64_t(v.BackoffMs) + if n_retryOn := len(v.RetryOn); n_retryOn > 0 { + arr_retryOn := C.malloc(C.size_t(n_retryOn) * C.size_t(unsafe.Sizeof((*C.char)(nil)))) + sl_retryOn := unsafe.Slice((**C.char)(arr_retryOn), n_retryOn) + for i := 0; i < n_retryOn; i++ { + cs_retryOn_e := C.CString(v.RetryOn[i]) + frees = append(frees, func() { C.free(unsafe.Pointer(cs_retryOn_e)) }) + sl_retryOn[i] = cs_retryOn_e + } + c.retryOn = (**C.char)(arr_retryOn) + c.retryOn_len = C.size_t(n_retryOn) + a_retryOn := arr_retryOn + frees = append(frees, func() { C.free(a_retryOn) }) + } + return c, frees +} + +// ScheduleConfig mirrors the {.ffi.} type of the same name. +type ScheduleConfig struct { + StartAtMs int64 + IntervalMs int64 + Jitter *int64 +} + +// toC marshals ScheduleConfig into its C-POD form, returning cleanup funcs to run after the call. +func (v ScheduleConfig) toC() (C.ScheduleConfig, []func()) { + var c C.ScheduleConfig + var frees []func() + c.startAtMs = C.int64_t(v.StartAtMs) + c.intervalMs = C.int64_t(v.IntervalMs) + if v.Jitter != nil { + c.jitter_present = 1 + c.jitter = C.int64_t(*v.Jitter) + } + return c, frees +} + +// ScheduleResult mirrors the {.ffi.} type of the same name. +type ScheduleResult struct { + JobId string + WillRunCount int64 + FirstRunAtMs int64 + EffectiveBackoffMs int64 +} + +// toC marshals ScheduleResult into its C-POD form, returning cleanup funcs to run after the call. +func (v ScheduleResult) toC() (C.ScheduleResult, []func()) { + var c C.ScheduleResult + var frees []func() + cs_jobId := C.CString(v.JobId) + frees = append(frees, func() { C.free(unsafe.Pointer(cs_jobId)) }) + c.jobId = cs_jobId + c.willRunCount = C.int64_t(v.WillRunCount) + c.firstRunAtMs = C.int64_t(v.FirstRunAtMs) + c.effectiveBackoffMs = C.int64_t(v.EffectiveBackoffMs) + return c, frees +} + +type My_timerNode struct { + ctx unsafe.Pointer +} + +// goStr extracts and frees the captured response string. +func respStr(r *C.My_timerResp) string { + return C.GoStringN(C.my_timerRespMsg(r), C.int(C.my_timerRespLen(r))) +} + +var ( + eventMu sync.Mutex + eventHandler func(string) +) + +// SetEventHandler installs the catch-all handler for library-initiated +// events (delivered as raw JSON strings). +func (n *My_timerNode) SetEventHandler(h func(string)) { + eventMu.Lock() + eventHandler = h + eventMu.Unlock() + C.my_timerRegisterEvents(n.ctx) +} + +//export my_timerGoEvent +func my_timerGoEvent(ret C.int, msg *C.char, length C.size_t, userData unsafe.Pointer) { + eventMu.Lock() + h := eventHandler + eventMu.Unlock() + if h != nil && ret == C.RET_OK { + h(C.GoStringN(msg, C.int(length))) + } +} + +func NewMy_timer(config TimerConfig) (*My_timerNode, error) { + c_config, free_config := config.toC() + defer func() { + for _, f := range free_config { + f() + } + }() + r := C.my_timerRespNew() + defer C.my_timerRespFree(r) + ctx := C.my_timerCall_my_timer_create(c_config, r) + if C.my_timerRespRet(r) != C.RET_OK { + return nil, errors.New(respStr(r)) + } + return &My_timerNode{ctx: ctx}, nil +} + +func (n *My_timerNode) Echo(req EchoRequest) (string, error) { + c_req, free_req := req.toC() + defer func() { + for _, f := range free_req { + f() + } + }() + r := C.my_timerRespNew() + defer C.my_timerRespFree(r) + C.my_timerCall_my_timer_echo(n.ctx, c_req, r) + if C.my_timerRespRet(r) != C.RET_OK { + return "", errors.New(respStr(r)) + } + return respStr(r), nil +} + +func (n *My_timerNode) Version() (string, error) { + r := C.my_timerRespNew() + defer C.my_timerRespFree(r) + C.my_timerCall_my_timer_version(n.ctx, r) + if C.my_timerRespRet(r) != C.RET_OK { + return "", errors.New(respStr(r)) + } + return respStr(r), nil +} + +func (n *My_timerNode) Complex(req ComplexRequest) (string, error) { + c_req, free_req := req.toC() + defer func() { + for _, f := range free_req { + f() + } + }() + r := C.my_timerRespNew() + defer C.my_timerRespFree(r) + C.my_timerCall_my_timer_complex(n.ctx, c_req, r) + if C.my_timerRespRet(r) != C.RET_OK { + return "", errors.New(respStr(r)) + } + return respStr(r), nil +} + +func (n *My_timerNode) Schedule(job JobSpec, retry RetryPolicy, schedule ScheduleConfig) (string, error) { + c_job, free_job := job.toC() + defer func() { + for _, f := range free_job { + f() + } + }() + c_retry, free_retry := retry.toC() + defer func() { + for _, f := range free_retry { + f() + } + }() + c_schedule, free_schedule := schedule.toC() + defer func() { + for _, f := range free_schedule { + f() + } + }() + r := C.my_timerRespNew() + defer C.my_timerRespFree(r) + C.my_timerCall_my_timer_schedule(n.ctx, c_job, c_retry, c_schedule, r) + if C.my_timerRespRet(r) != C.RET_OK { + return "", errors.New(respStr(r)) + } + return respStr(r), nil +} + +func (n *My_timerNode) Destroy() error { + if C.my_timerCall_my_timer_destroy(n.ctx) != C.RET_OK { + return errors.New("my_timer destroy failed") + } + return nil +} diff --git a/examples/timer/go_bindings/my_timer.h b/examples/timer/go_bindings/my_timer.h new file mode 100644 index 0000000..d5ad5fa --- /dev/null +++ b/examples/timer/go_bindings/my_timer.h @@ -0,0 +1,116 @@ +// Generated by nim-ffi C codegen. Do not edit by hand. +// +// Native (zero-serialization) C ABI. Each call delivers its result to the +// callback: on RET_OK, (msg, len) is the raw return value (for string-returning +// procs, the string bytes — not NUL-terminated; use len); on RET_ERR, (msg, len) +// is the raw error text. A `_cbor` variant of each proc also exists for +// generic/cross-language callers that prefer a CBOR request/response. +#ifndef NIM_FFI_GEN_MY_TIMER_H +#define NIM_FFI_GEN_MY_TIMER_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef NIM_FFI_RET_CODES +#define NIM_FFI_RET_CODES +#define RET_OK 0 +#define RET_ERR 1 +#define RET_MISSING_CALLBACK 2 +#endif + +#ifndef NIM_FFI_CALLBACK_T +#define NIM_FFI_CALLBACK_T +typedef void (*FFICallBack)(int callerRet, const char *msg, size_t len, void *userData); +#endif + + +// --- {.ffi.}-annotated types, exposed as C structs ---------- +typedef struct { + const char* name; +} TimerConfig; + +typedef struct { + const char* message; + int64_t delayMs; +} EchoRequest; + +typedef struct { + const char* echoed; + const char* timerName; +} EchoResponse; + +typedef struct { + EchoRequest *messages; + size_t messages_len; + const char* *tags; + size_t tags_len; + int note_present; + const char* note; + int retries_present; + int64_t retries; +} ComplexRequest; + +typedef struct { + const char* summary; + int64_t itemCount; + int hasNote; +} ComplexResponse; + +typedef struct { + const char* message; + int64_t echoCount; +} EchoEvent; + +typedef struct { + const char* name; + const char* *payload; + size_t payload_len; + int64_t priority; +} JobSpec; + +typedef struct { + int64_t maxAttempts; + int64_t backoffMs; + const char* *retryOn; + size_t retryOn_len; +} RetryPolicy; + +typedef struct { + int64_t startAtMs; + int64_t intervalMs; + int jitter_present; + int64_t jitter; +} ScheduleConfig; + +typedef struct { + const char* jobId; + int64_t willRunCount; + int64_t firstRunAtMs; + int64_t effectiveBackoffMs; +} ScheduleResult; + + +void *my_timer_create(TimerConfig config, FFICallBack callback, void *userData); + +int my_timer_echo(void *ctx, FFICallBack callback, void *userData, EchoRequest req); + +int my_timer_version(void *ctx, FFICallBack callback, void *userData); + +int my_timer_complex(void *ctx, FFICallBack callback, void *userData, ComplexRequest req); + +int my_timer_schedule(void *ctx, FFICallBack callback, void *userData, JobSpec job, RetryPolicy retry, ScheduleConfig schedule); + +int my_timer_destroy(void *ctx); + +uint64_t my_timer_add_event_listener(void *ctx, const char *eventName, FFICallBack callback, void *userData); +int my_timer_remove_event_listener(void *ctx, uint64_t listenerId); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif /* NIM_FFI_GEN_MY_TIMER_H */ \ No newline at end of file diff --git a/ffi.nimble b/ffi.nimble index 11621c5..d6b3215 100644 --- a/ffi.nimble +++ b/ffi.nimble @@ -167,6 +167,10 @@ task genbindings_go, "Generate Go (cgo) bindings for the timer example": " -d:ffiOutputDir=examples/timer/go_bindings" & " -d:ffiSrcPath=../timer.nim" & " -o:/dev/null examples/timer/timer.nim" + # The codegen emits compilable but not column-aligned Go; gofmt finalizes it + # (cgo struct-field alignment etc.). Skipped silently if gofmt isn't present. + if findExe("gofmt").len > 0: + exec "gofmt -w examples/timer/go_bindings/my_timer.go" task genbindings_cddl, "Generate CDDL schema for the timer example": exec "nim c " & nimFlagsOrc & diff --git a/ffi/codegen/go.nim b/ffi/codegen/go.nim index d808ae8..c992c6f 100644 --- a/ffi/codegen/go.nim +++ b/ffi/codegen/go.nim @@ -22,6 +22,7 @@ import std/[os, strutils] import ./meta, ./string_helpers +import ./c as cgen proc nimTypeToGo(typeName: string): string = let t = typeName.strip() @@ -75,9 +76,63 @@ proc cgoArgType(typeName: string): string = proc supported(typeName: string): bool = cgoArgType(typeName).len > 0 or typeName.strip() in ["string", "cstring"] -proc allSupported(p: FFIProcMeta): bool = +# --- {.ffi.}-struct param support ------------------------------------------- +# A registered {.ffi.} type is passed to the native ABI as a flat C-POD struct +# (see codegen/c.emitCStructs); cgo exposes it as `C.`. We mirror each +# type as an idiomatic Go struct and marshal it into the C struct per call, +# freeing the C-side allocations once the call returns (the native path deep- +# copies every argument, so the C buffers are safe to release immediately). + +proc isFFIStruct(typeName: string, types: seq[FFITypeMeta]): bool = + let t = typeName.strip() + for ty in types: + if ty.name == t: + return true + return false + +proc isSeqT(t: string): bool = + t.strip().startsWith("seq[") and t.strip().endsWith("]") + +proc isOptT(t: string): bool = + let s = t.strip() + (s.startsWith("Option[") or s.startsWith("Maybe[")) and s.endsWith("]") + +proc seqElemT(t: string): string = + t.strip()["seq[".len .. ^2].strip() + +proc optElemT(t: string): string = + let s = t.strip() + let p = if s.startsWith("Maybe["): "Maybe[".len else: "Option[".len + s[p .. ^2].strip() + +proc isStringT(t: string): bool = + t.strip() in ["string", "cstring"] + +proc goFieldType(typeName: string, types: seq[FFITypeMeta]): string = + ## Idiomatic Go type for a struct field: seq -> slice, Option -> pointer, + ## string -> string, scalar -> mapped, nested {.ffi.} struct -> its Go name. + let t = typeName.strip() + if isSeqT(t): + "[]" & goFieldType(seqElemT(t), types) + elif isOptT(t): + "*" & goFieldType(optElemT(t), types) + else: + nimTypeToGo(t) + +proc cgoElemType(typeName: string, types: seq[FFITypeMeta]): string = + ## The cgo type for one scalar/element value. + let t = typeName.strip() + if isFFIStruct(t, types): "C." & t + elif isStringT(t): "*C.char" + else: cgoArgType(t) + +proc paramSupported(typeName: string, types: seq[FFITypeMeta]): bool = + let t = typeName.strip() + supported(t) or isFFIStruct(t, types) + +proc allSupported(p: FFIProcMeta, types: seq[FFITypeMeta]): bool = for ep in p.extraParams: - if ep.isPtr or not supported(ep.typeName): + if ep.isPtr or not paramSupported(ep.typeName, types): return false return true @@ -90,6 +145,136 @@ proc methodName(procName, libName: string): string = procName return snakeToPascalCase(bare) +# --- Go marshalling code generators ----------------------------------------- +# All emit tab-indented Go lines. `dst` is a cgo l-value, `src` a Go expression. +# String/struct/seq marshalling appends a cleanup to the local `frees` slice. + +proc marshalValue( + dst, src, typeName: string, types: seq[FFITypeMeta], tok: string +): seq[string] = + ## Convert a single Go value `src` into the cgo field `dst`. + var lines: seq[string] = @[] + let t = typeName.strip() + if isStringT(t): + lines.add("\tcs_" & tok & " := C.CString(" & src & ")") + lines.add( + "\tfrees = append(frees, func() { C.free(unsafe.Pointer(cs_" & tok & ")) })" + ) + lines.add("\t" & dst & " = cs_" & tok) + elif isFFIStruct(t, types): + lines.add("\tcf_" & tok & ", ff_" & tok & " := (" & src & ").toC()") + lines.add("\tfrees = append(frees, ff_" & tok & "...)") + lines.add("\t" & dst & " = cf_" & tok) + elif t == "bool": + lines.add("\tif " & src & " {") + lines.add("\t\t" & dst & " = 1") + lines.add("\t} else {") + lines.add("\t\t" & dst & " = 0") + lines.add("\t}") + else: + lines.add("\t" & dst & " = " & cgoArgType(t) & "(" & src & ")") + return lines + +proc cgoZeroVal(typeName: string, types: seq[FFITypeMeta]): string = + ## A zero value of the element type, for `unsafe.Sizeof` in C array allocation. + let t = typeName.strip() + if isFFIStruct(t, types): "C." & t & "{}" + elif isStringT(t): "(*C.char)(nil)" + else: cgoArgType(t) & "(0)" + +proc goMarshalField(f: FFIFieldMeta, types: seq[FFITypeMeta]): seq[string] = + ## Populate `c.` (and `c._len` / `c._present`) from + ## `v.`, matching the C-POD layout in codegen/c.emitCStructs. + var lines: seq[string] = @[] + let cname = f.name + let gofld = capitalizeFirstLetter(f.name) + let t = f.typeName.strip() + if isSeqT(t): + let elem = seqElemT(t) + let cElem = cgoElemType(elem, types) + lines.add("\tif n_" & cname & " := len(v." & gofld & "); n_" & cname & " > 0 {") + lines.add( + "\t\tarr_" & cname & " := C.malloc(C.size_t(n_" & cname & + ") * C.size_t(unsafe.Sizeof(" & cgoZeroVal(elem, types) & ")))" + ) + lines.add( + "\t\tsl_" & cname & " := unsafe.Slice((*" & cElem & ")(arr_" & cname & "), n_" & + cname & ")" + ) + lines.add("\t\tfor i := 0; i < n_" & cname & "; i++ {") + for ln in marshalValue( + "sl_" & cname & "[i]", "v." & gofld & "[i]", elem, types, cname & "_e" + ): + lines.add("\t\t" & ln) + lines.add("\t\t}") + lines.add("\t\tc." & cname & " = (*" & cElem & ")(arr_" & cname & ")") + lines.add("\t\tc." & cname & "_len = C.size_t(n_" & cname & ")") + lines.add("\t\ta_" & cname & " := arr_" & cname) + lines.add("\t\tfrees = append(frees, func() { C.free(a_" & cname & ") })") + lines.add("\t}") + elif isOptT(t): + let elem = optElemT(t) + lines.add("\tif v." & gofld & " != nil {") + lines.add("\t\tc." & cname & "_present = 1") + for ln in marshalValue("c." & cname, "*v." & gofld, elem, types, cname & "_o"): + lines.add("\t" & ln) + lines.add("\t}") + else: + for ln in marshalValue("c." & cname, "v." & gofld, t, types, cname): + lines.add(ln) + return lines + +proc emitGoTypesAndToC(types: seq[FFITypeMeta]): seq[string] = + ## A Go struct + `toC()` marshaller for every {.ffi.} type. + var lines: seq[string] = @[] + for ty in types: + lines.add("// " & ty.name & " mirrors the {.ffi.} type of the same name.") + lines.add("type " & ty.name & " struct {") + for f in ty.fields: + lines.add("\t" & capitalizeFirstLetter(f.name) & " " & goFieldType(f.typeName, types)) + lines.add("}") + lines.add("") + lines.add( + "// toC marshals " & ty.name & + " into its C-POD form, returning cleanup funcs to run after the call." + ) + lines.add("func (v " & ty.name & ") toC() (C." & ty.name & ", []func()) {") + lines.add("\tvar c C." & ty.name) + lines.add("\tvar frees []func()") + for f in ty.fields: + for ln in goMarshalField(f, types): + lines.add(ln) + lines.add("\treturn c, frees") + lines.add("}") + lines.add("") + return lines + +proc goParamConv( + extraParams: seq[FFIParamMeta], types: seq[FFITypeMeta] +): tuple[goParams, conv, callArgs: seq[string]] = + ## Go method/ctor parameter list, the conversion lines that turn each Go arg + ## into a cgo value, and the resulting call-argument expressions. + var goParams, conv, callArgs: seq[string] = @[] + for ep in extraParams: + let nm = ep.name + let t = ep.typeName.strip() + goParams.add(nm & " " & goFieldType(t, types)) + if isFFIStruct(t, types): + conv.add("\tc_" & nm & ", free_" & nm & " := " & nm & ".toC()") + conv.add("\tdefer func() { for _, f := range free_" & nm & " { f() } }()") + callArgs.add("c_" & nm) + elif isStringT(t): + conv.add("\tc_" & nm & " := C.CString(" & nm & ")") + conv.add("\tdefer C.free(unsafe.Pointer(c_" & nm & "))") + callArgs.add("c_" & nm) + elif t == "bool": + conv.add("\tc_" & nm & " := C.int(0)") + conv.add("\tif " & nm & " { c_" & nm & " = 1 }") + callArgs.add("c_" & nm) + else: + callArgs.add(cgoArgType(t) & "(" & nm & ")") + return (goParams, conv, callArgs) + proc generateGoFile*( procs: seq[FFIProcMeta], types: seq[FFITypeMeta], @@ -116,7 +301,11 @@ proc generateGoFile*( L.add("package " & libName) L.add("") L.add("/*") - L.add("#cgo LDFLAGS: -l" & libName) + # ${SRCDIR} expands to this package's directory, so the native header and the + # built lib (staged next to the .go) are found without extra env vars; the + # rpath lets the dylib/.so load at runtime from the same directory. + L.add("#cgo CFLAGS: -I${SRCDIR}") + L.add("#cgo LDFLAGS: -L${SRCDIR} -l" & libName & " -Wl,-rpath,${SRCDIR}") L.add("#include \"" & libName & ".h\"") L.add("#include ") L.add("#include ") @@ -199,7 +388,7 @@ proc generateGoFile*( # per-proc bridges for p in procs: - if p.kind != FFIKind.FFI or not allSupported(p): + if p.kind != FFIKind.FFI or not allSupported(p, types): continue var cparams: seq[string] = @[] var callArgs: seq[string] = @[] @@ -247,6 +436,10 @@ proc generateGoFile*( L.add(")") L.add("") + # ---- Go mirrors of the {.ffi.} types (with C-POD marshalling) ------------ + for line in emitGoTypesAndToC(types): + L.add(line) + # ---- Go types + helpers -------------------------------------------------- L.add("type " & nodeType & " struct {") L.add("\tctx unsafe.Pointer") @@ -292,17 +485,7 @@ proc generateGoFile*( # ---- constructor --------------------------------------------------------- if haveCtor: - var goParams: seq[string] = @[] - var conv: seq[string] = @[] - var callArgs: seq[string] = @[] - for ep in ctor.extraParams: - goParams.add(ep.name & " " & nimTypeToGo(ep.typeName)) - if nimTypeToGo(ep.typeName) == "string": - conv.add("\tc_" & ep.name & " := C.CString(" & ep.name & ")") - conv.add("\tdefer C.free(unsafe.Pointer(c_" & ep.name & "))") - callArgs.add("c_" & ep.name) - else: - callArgs.add(cgoArgType(ep.typeName) & "(" & ep.name & ")") + let (goParams, conv, callArgs) = goParamConv(ctor.extraParams, types) let callArgsStr = if callArgs.len > 0: callArgs.join(", ") & ", " @@ -328,25 +511,15 @@ proc generateGoFile*( for p in procs: if p.kind != FFIKind.FFI: continue - if not allSupported(p): + if not allSupported(p, types): L.add( "// SKIPPED " & p.procName & - ": struct/seq/Option params unsupported by Go codegen" + ": unsupported param (raw pointer or bare seq/Option) for Go codegen" ) L.add("") continue let mName = methodName(p.procName, libName) - var goParams: seq[string] = @[] - var conv: seq[string] = @[] - var callArgs: seq[string] = @[] - for ep in p.extraParams: - goParams.add(ep.name & " " & nimTypeToGo(ep.typeName)) - if nimTypeToGo(ep.typeName) == "string": - conv.add("\tc_" & ep.name & " := C.CString(" & ep.name & ")") - conv.add("\tdefer C.free(unsafe.Pointer(c_" & ep.name & "))") - callArgs.add("c_" & ep.name) - else: - callArgs.add(cgoArgType(ep.typeName) & "(" & ep.name & ")") + let (goParams, conv, callArgs) = goParamConv(p.extraParams, types) let callArgsStr = if callArgs.len > 0: ", " & callArgs.join(", ") @@ -391,6 +564,12 @@ proc generateGoBindings*( writeFile( outputDir / (libName & ".go"), generateGoFile(procs, types, libName, events) ) + # cgo `#include ".h"` resolves against this package directory, so emit the + # native C header here too — the Go package is then self-contained (just stage + # the built lib next to it). + writeFile( + outputDir / (libName & ".h"), generateCHeader(procs, types, libName, events) + ) # Emit a go.mod so the generated package is an importable module (parity with # the Rust/C++ generators that emit Cargo.toml / CMakeLists.txt). Consumers can # `replace => `.