mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-20 16:29:31 +00:00
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.<Type>` (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 `<lib>.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 <noreply@anthropic.com>
This commit is contained in:
parent
efadf11660
commit
965fd68785
3
examples/timer/go_bindings/go.mod
Normal file
3
examples/timer/go_bindings/go.mod
Normal file
@ -0,0 +1,3 @@
|
||||
module my_timer
|
||||
|
||||
go 1.21
|
||||
452
examples/timer/go_bindings/my_timer.go
Normal file
452
examples/timer/go_bindings/my_timer.go
Normal file
@ -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 <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <pthread.h>
|
||||
|
||||
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
|
||||
}
|
||||
116
examples/timer/go_bindings/my_timer.h
Normal file
116
examples/timer/go_bindings/my_timer.h
Normal file
@ -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 `<name>_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 <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#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 */
|
||||
@ -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 &
|
||||
|
||||
@ -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.<Type>`. 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.<field>` (and `c.<field>_len` / `c.<field>_present`) from
|
||||
## `v.<Field>`, 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 <stdlib.h>")
|
||||
L.add("#include <string.h>")
|
||||
@ -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 "<lib>.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 <libName> => <path to this dir>`.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user