diff --git a/examples/timer/go_bindings/my_timer.go b/examples/timer/go_bindings/my_timer.go index 43d8486..41ae0b9 100644 --- a/examples/timer/go_bindings/my_timer.go +++ b/examples/timer/go_bindings/my_timer.go @@ -10,6 +10,9 @@ package my_timer #include extern void my_timerGoEvent(int ret, char* msg, size_t len, void* userData); +extern void my_timerResultEcho(int ret, char* msg, size_t len, void* ud); +extern void my_timerResultComplex(int ret, char* msg, size_t len, void* ud); +extern void my_timerResultSchedule(int ret, char* msg, size_t len, void* ud); typedef struct { int ret; char* msg; size_t len; int done; @@ -51,26 +54,11 @@ static void* my_timerCall_my_timer_create(TimerConfig config, My_timerResp* 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); } */ @@ -78,10 +66,17 @@ import "C" import ( "errors" + "runtime/cgo" "sync" "unsafe" ) +type resultSlot struct { + val any + err error + done chan struct{} +} + // TimerConfig mirrors the {.ffi.} type of the same name. type TimerConfig struct { Name string @@ -97,6 +92,13 @@ func (v TimerConfig) toC() (C.TimerConfig, []func()) { return c, frees } +// TimerConfigFromC copies a C-POD TimerConfig (e.g. a typed return) into a Go value. +func TimerConfigFromC(c *C.TimerConfig) TimerConfig { + var v TimerConfig + v.Name = C.GoString(c.name) + return v +} + // EchoRequest mirrors the {.ffi.} type of the same name. type EchoRequest struct { Message string @@ -114,6 +116,14 @@ func (v EchoRequest) toC() (C.EchoRequest, []func()) { return c, frees } +// EchoRequestFromC copies a C-POD EchoRequest (e.g. a typed return) into a Go value. +func EchoRequestFromC(c *C.EchoRequest) EchoRequest { + var v EchoRequest + v.Message = C.GoString(c.message) + v.DelayMs = int64(c.delayMs) + return v +} + // EchoResponse mirrors the {.ffi.} type of the same name. type EchoResponse struct { Echoed string @@ -133,6 +143,14 @@ func (v EchoResponse) toC() (C.EchoResponse, []func()) { return c, frees } +// EchoResponseFromC copies a C-POD EchoResponse (e.g. a typed return) into a Go value. +func EchoResponseFromC(c *C.EchoResponse) EchoResponse { + var v EchoResponse + v.Echoed = C.GoString(c.echoed) + v.TimerName = C.GoString(c.timerName) + return v +} + // ComplexRequest mirrors the {.ffi.} type of the same name. type ComplexRequest struct { Messages []EchoRequest @@ -184,6 +202,34 @@ func (v ComplexRequest) toC() (C.ComplexRequest, []func()) { return c, frees } +// ComplexRequestFromC copies a C-POD ComplexRequest (e.g. a typed return) into a Go value. +func ComplexRequestFromC(c *C.ComplexRequest) ComplexRequest { + var v ComplexRequest + if c.messages_len > 0 { + src_messages := unsafe.Slice(c.messages, int(c.messages_len)) + v.Messages = make([]EchoRequest, int(c.messages_len)) + for i := range src_messages { + v.Messages[i] = EchoRequestFromC(&src_messages[i]) + } + } + if c.tags_len > 0 { + src_tags := unsafe.Slice(c.tags, int(c.tags_len)) + v.Tags = make([]string, int(c.tags_len)) + for i := range src_tags { + v.Tags[i] = C.GoString(src_tags[i]) + } + } + if c.note_present != 0 { + tmp_note := C.GoString(c.note) + v.Note = &tmp_note + } + if c.retries_present != 0 { + tmp_retries := int64(c.retries) + v.Retries = &tmp_retries + } + return v +} + // ComplexResponse mirrors the {.ffi.} type of the same name. type ComplexResponse struct { Summary string @@ -207,6 +253,15 @@ func (v ComplexResponse) toC() (C.ComplexResponse, []func()) { return c, frees } +// ComplexResponseFromC copies a C-POD ComplexResponse (e.g. a typed return) into a Go value. +func ComplexResponseFromC(c *C.ComplexResponse) ComplexResponse { + var v ComplexResponse + v.Summary = C.GoString(c.summary) + v.ItemCount = int64(c.itemCount) + v.HasNote = (c.hasNote != 0) + return v +} + // EchoEvent mirrors the {.ffi.} type of the same name. type EchoEvent struct { Message string @@ -224,6 +279,14 @@ func (v EchoEvent) toC() (C.EchoEvent, []func()) { return c, frees } +// EchoEventFromC copies a C-POD EchoEvent (e.g. a typed return) into a Go value. +func EchoEventFromC(c *C.EchoEvent) EchoEvent { + var v EchoEvent + v.Message = C.GoString(c.message) + v.EchoCount = int64(c.echoCount) + return v +} + // JobSpec mirrors the {.ffi.} type of the same name. type JobSpec struct { Name string @@ -255,6 +318,21 @@ func (v JobSpec) toC() (C.JobSpec, []func()) { return c, frees } +// JobSpecFromC copies a C-POD JobSpec (e.g. a typed return) into a Go value. +func JobSpecFromC(c *C.JobSpec) JobSpec { + var v JobSpec + v.Name = C.GoString(c.name) + if c.payload_len > 0 { + src_payload := unsafe.Slice(c.payload, int(c.payload_len)) + v.Payload = make([]string, int(c.payload_len)) + for i := range src_payload { + v.Payload[i] = C.GoString(src_payload[i]) + } + } + v.Priority = int64(c.priority) + return v +} + // RetryPolicy mirrors the {.ffi.} type of the same name. type RetryPolicy struct { MaxAttempts int64 @@ -284,6 +362,21 @@ func (v RetryPolicy) toC() (C.RetryPolicy, []func()) { return c, frees } +// RetryPolicyFromC copies a C-POD RetryPolicy (e.g. a typed return) into a Go value. +func RetryPolicyFromC(c *C.RetryPolicy) RetryPolicy { + var v RetryPolicy + v.MaxAttempts = int64(c.maxAttempts) + v.BackoffMs = int64(c.backoffMs) + if c.retryOn_len > 0 { + src_retryOn := unsafe.Slice(c.retryOn, int(c.retryOn_len)) + v.RetryOn = make([]string, int(c.retryOn_len)) + for i := range src_retryOn { + v.RetryOn[i] = C.GoString(src_retryOn[i]) + } + } + return v +} + // ScheduleConfig mirrors the {.ffi.} type of the same name. type ScheduleConfig struct { StartAtMs int64 @@ -304,6 +397,18 @@ func (v ScheduleConfig) toC() (C.ScheduleConfig, []func()) { return c, frees } +// ScheduleConfigFromC copies a C-POD ScheduleConfig (e.g. a typed return) into a Go value. +func ScheduleConfigFromC(c *C.ScheduleConfig) ScheduleConfig { + var v ScheduleConfig + v.StartAtMs = int64(c.startAtMs) + v.IntervalMs = int64(c.intervalMs) + if c.jitter_present != 0 { + tmp_jitter := int64(c.jitter) + v.Jitter = &tmp_jitter + } + return v +} + // ScheduleResult mirrors the {.ffi.} type of the same name. type ScheduleResult struct { JobId string @@ -325,6 +430,16 @@ func (v ScheduleResult) toC() (C.ScheduleResult, []func()) { return c, frees } +// ScheduleResultFromC copies a C-POD ScheduleResult (e.g. a typed return) into a Go value. +func ScheduleResultFromC(c *C.ScheduleResult) ScheduleResult { + var v ScheduleResult + v.JobId = C.GoString(c.jobId) + v.WillRunCount = int64(c.willRunCount) + v.FirstRunAtMs = int64(c.firstRunAtMs) + v.EffectiveBackoffMs = int64(c.effectiveBackoffMs) + return v +} + type My_timerNode struct { ctx unsafe.Pointer } @@ -374,20 +489,39 @@ func NewMy_timer(config TimerConfig) (*My_timerNode, error) { return &My_timerNode{ctx: ctx}, nil } -func (n *My_timerNode) Echo(req EchoRequest) (string, error) { +//export my_timerResultEcho +func my_timerResultEcho(ret C.int, msg *C.char, length C.size_t, ud unsafe.Pointer) { + slot := cgo.Handle(*(*C.uintptr_t)(ud)).Value().(*resultSlot) + if ret == C.RET_OK { + slot.val = EchoResponseFromC((*C.EchoResponse)(unsafe.Pointer(msg))) + } else { + slot.err = errors.New(C.GoStringN(msg, C.int(length))) + } + close(slot.done) +} + +func (n *My_timerNode) Echo(req EchoRequest) (EchoResponse, 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)) + slot := &resultSlot{done: make(chan struct{})} + h := cgo.NewHandle(slot) + defer h.Delete() + hbox := (*C.uintptr_t)(C.malloc(C.size_t(unsafe.Sizeof(C.uintptr_t(0))))) + *hbox = C.uintptr_t(h) + defer C.free(unsafe.Pointer(hbox)) + rc := C.my_timer_echo(n.ctx, C.FFICallBack(C.my_timerResultEcho), unsafe.Pointer(hbox), c_req) + if rc != C.RET_OK { + return EchoResponse{}, errors.New("my_timer_echo: dispatch failed") } - return respStr(r), nil + <-slot.done + if slot.err != nil { + return EchoResponse{}, slot.err + } + return slot.val.(EchoResponse), nil } func (n *My_timerNode) Version() (string, error) { @@ -400,23 +534,53 @@ func (n *My_timerNode) Version() (string, error) { return respStr(r), nil } -func (n *My_timerNode) Complex(req ComplexRequest) (string, error) { +//export my_timerResultComplex +func my_timerResultComplex(ret C.int, msg *C.char, length C.size_t, ud unsafe.Pointer) { + slot := cgo.Handle(*(*C.uintptr_t)(ud)).Value().(*resultSlot) + if ret == C.RET_OK { + slot.val = ComplexResponseFromC((*C.ComplexResponse)(unsafe.Pointer(msg))) + } else { + slot.err = errors.New(C.GoStringN(msg, C.int(length))) + } + close(slot.done) +} + +func (n *My_timerNode) Complex(req ComplexRequest) (ComplexResponse, 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)) + slot := &resultSlot{done: make(chan struct{})} + h := cgo.NewHandle(slot) + defer h.Delete() + hbox := (*C.uintptr_t)(C.malloc(C.size_t(unsafe.Sizeof(C.uintptr_t(0))))) + *hbox = C.uintptr_t(h) + defer C.free(unsafe.Pointer(hbox)) + rc := C.my_timer_complex(n.ctx, C.FFICallBack(C.my_timerResultComplex), unsafe.Pointer(hbox), c_req) + if rc != C.RET_OK { + return ComplexResponse{}, errors.New("my_timer_complex: dispatch failed") } - return respStr(r), nil + <-slot.done + if slot.err != nil { + return ComplexResponse{}, slot.err + } + return slot.val.(ComplexResponse), nil } -func (n *My_timerNode) Schedule(job JobSpec, retry RetryPolicy, schedule ScheduleConfig) (string, error) { +//export my_timerResultSchedule +func my_timerResultSchedule(ret C.int, msg *C.char, length C.size_t, ud unsafe.Pointer) { + slot := cgo.Handle(*(*C.uintptr_t)(ud)).Value().(*resultSlot) + if ret == C.RET_OK { + slot.val = ScheduleResultFromC((*C.ScheduleResult)(unsafe.Pointer(msg))) + } else { + slot.err = errors.New(C.GoStringN(msg, C.int(length))) + } + close(slot.done) +} + +func (n *My_timerNode) Schedule(job JobSpec, retry RetryPolicy, schedule ScheduleConfig) (ScheduleResult, error) { c_job, free_job := job.toC() defer func() { for _, f := range free_job { @@ -435,13 +599,21 @@ func (n *My_timerNode) Schedule(job JobSpec, retry RetryPolicy, schedule Schedul 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)) + slot := &resultSlot{done: make(chan struct{})} + h := cgo.NewHandle(slot) + defer h.Delete() + hbox := (*C.uintptr_t)(C.malloc(C.size_t(unsafe.Sizeof(C.uintptr_t(0))))) + *hbox = C.uintptr_t(h) + defer C.free(unsafe.Pointer(hbox)) + rc := C.my_timer_schedule(n.ctx, C.FFICallBack(C.my_timerResultSchedule), unsafe.Pointer(hbox), c_job, c_retry, c_schedule) + if rc != C.RET_OK { + return ScheduleResult{}, errors.New("my_timer_schedule: dispatch failed") } - return respStr(r), nil + <-slot.done + if slot.err != nil { + return ScheduleResult{}, slot.err + } + return slot.val.(ScheduleResult), nil } func (n *My_timerNode) Destroy() error { diff --git a/ffi/codegen/go.nim b/ffi/codegen/go.nim index c992c6f..2234329 100644 --- a/ffi/codegen/go.nim +++ b/ffi/codegen/go.nim @@ -224,8 +224,50 @@ proc goMarshalField(f: FFIFieldMeta, types: seq[FFITypeMeta]): seq[string] = lines.add(ln) return lines +# --- reverse marshalling: C-POD -> Go (for typed struct returns) ------------ +# Used inside the result callback, where the POD is still alive; everything is +# copied out into Go-owned memory before the library frees the POD. + +proc fromCExpr(src, typeName: string, types: seq[FFITypeMeta]): string = + ## A Go expression converting the cgo value `src` to its Go type. + let t = typeName.strip() + if isStringT(t): "C.GoString(" & src & ")" + elif isFFIStruct(t, types): t & "FromC(&" & src & ")" + elif t == "bool": "(" & src & " != 0)" + else: nimTypeToGo(t) & "(" & src & ")" + +proc goUnmarshalField(f: FFIFieldMeta, types: seq[FFITypeMeta]): seq[string] = + ## Copy `c.` (+ `_len` / `_present`) out into `v.`. + 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 elemGo = goFieldType(elem, types) + lines.add("\tif c." & cname & "_len > 0 {") + lines.add( + "\t\tsrc_" & cname & " := unsafe.Slice(c." & cname & ", int(c." & cname & "_len))" + ) + lines.add("\t\tv." & gofld & " = make([]" & elemGo & ", int(c." & cname & "_len))") + lines.add("\t\tfor i := range src_" & cname & " {") + lines.add( + "\t\t\tv." & gofld & "[i] = " & fromCExpr("src_" & cname & "[i]", elem, types) + ) + lines.add("\t\t}") + lines.add("\t}") + elif isOptT(t): + let elem = optElemT(t) + lines.add("\tif c." & cname & "_present != 0 {") + lines.add("\t\ttmp_" & cname & " := " & fromCExpr("c." & cname, elem, types)) + lines.add("\t\tv." & gofld & " = &tmp_" & cname) + lines.add("\t}") + else: + lines.add("\tv." & gofld & " = " & fromCExpr("c." & cname, t, types)) + return lines + proc emitGoTypesAndToC(types: seq[FFITypeMeta]): seq[string] = - ## A Go struct + `toC()` marshaller for every {.ffi.} type. + ## A Go struct + `toC()` marshaller + `fromC()` reader for every {.ffi.} type. var lines: seq[string] = @[] for ty in types: lines.add("// " & ty.name & " mirrors the {.ffi.} type of the same name.") @@ -247,6 +289,18 @@ proc emitGoTypesAndToC(types: seq[FFITypeMeta]): seq[string] = lines.add("\treturn c, frees") lines.add("}") lines.add("") + lines.add( + "// " & ty.name & "FromC copies a C-POD " & ty.name & " (e.g. a typed return" & + ") into a Go value." + ) + lines.add("func " & ty.name & "FromC(c *C." & ty.name & ") " & ty.name & " {") + lines.add("\tvar v " & ty.name) + for f in ty.fields: + for ln in goUnmarshalField(f, types): + lines.add(ln) + lines.add("\treturn v") + lines.add("}") + lines.add("") return lines proc goParamConv( @@ -316,6 +370,15 @@ proc generateGoFile*( L.add( "extern void " & libName & "GoEvent(int ret, char* msg, size_t len, void* userData);" ) + # One exported Go result callback per struct-returning proc (it reads the typed + # return POD in-callback). Forward-declared here so cgo's `char*` shape matches. + for p in procs: + if p.kind == FFIKind.FFI and allSupported(p, types) and + isFFIStruct(p.returnTypeName, types): + L.add( + "extern void " & libName & "Result" & methodName(p.procName, libName) & + "(int ret, char* msg, size_t len, void* ud);" + ) L.add("") L.add("typedef struct {") L.add(" int ret; char* msg; size_t len; int done;") @@ -386,10 +449,14 @@ proc generateGoFile*( L.add(" return ctx;") L.add("}") - # per-proc bridges + # per-proc bridges — only for string/raw-returning procs. Struct-returning + # procs call the native entry point directly from Go with an exported Go + # callback (the C RespCb would copy struct bytes whose pointers then dangle). for p in procs: if p.kind != FFIKind.FFI or not allSupported(p, types): continue + if isFFIStruct(p.returnTypeName, types): + continue var cparams: seq[string] = @[] var callArgs: seq[string] = @[] for ep in p.extraParams: @@ -431,10 +498,19 @@ proc generateGoFile*( L.add("") L.add("import (") L.add("\t\"errors\"") + L.add("\t\"runtime/cgo\"") L.add("\t\"sync\"") L.add("\t\"unsafe\"") L.add(")") L.add("") + # Carries one typed result from the FFI-thread callback back to the blocked + # caller goroutine; passed through C as a cgo.Handle token (stable under GC). + L.add("type resultSlot struct {") + L.add("\tval any") + L.add("\terr error") + L.add("\tdone chan struct{}") + L.add("}") + L.add("") # ---- Go mirrors of the {.ffi.} types (with C-POD marshalling) ------------ for line in emitGoTypesAndToC(types): @@ -525,21 +601,75 @@ proc generateGoFile*( ", " & callArgs.join(", ") else: "" - L.add( - "func (n *" & nodeType & ") " & mName & "(" & goParams.join(", ") & - ") (string, error) {" - ) - for c in conv: - L.add(c) - L.add("\tr := C." & libName & "RespNew()") - L.add("\tdefer C." & libName & "RespFree(r)") - L.add("\tC." & libName & "Call_" & p.procName & "(n.ctx" & callArgsStr & ", r)") - L.add("\tif C." & libName & "RespRet(r) != C.RET_OK {") - L.add("\t\treturn \"\", errors.New(respStr(r))") - L.add("\t}") - L.add("\treturn respStr(r), nil") - L.add("}") - L.add("") + + if isFFIStruct(p.returnTypeName, types): + # Typed struct return: call the native entry point directly with an + # exported Go callback; a cgo.Handle token carries the result slot through + # C (stable under GC) so the async callback can deliver the typed value. + let retT = p.returnTypeName + let cbName = libName & "Result" & mName + L.add("//export " & cbName) + L.add( + "func " & cbName & + "(ret C.int, msg *C.char, length C.size_t, ud unsafe.Pointer) {" + ) + L.add("\tslot := cgo.Handle(*(*C.uintptr_t)(ud)).Value().(*resultSlot)") + L.add("\tif ret == C.RET_OK {") + L.add("\t\tslot.val = " & retT & "FromC((*C." & retT & ")(unsafe.Pointer(msg)))") + L.add("\t} else {") + L.add("\t\tslot.err = errors.New(C.GoStringN(msg, C.int(length)))") + L.add("\t}") + L.add("\tclose(slot.done)") + L.add("}") + L.add("") + L.add( + "func (n *" & nodeType & ") " & mName & "(" & goParams.join(", ") & ") (" & + retT & ", error) {" + ) + for c in conv: + L.add(c) + L.add("\tslot := &resultSlot{done: make(chan struct{})}") + L.add("\th := cgo.NewHandle(slot)") + L.add("\tdefer h.Delete()") + # Box the cgo.Handle in a small C allocation and pass that real C pointer as + # the void* userData. The handle is a stable GC-safe token; boxing it (vs. + # casting it straight to unsafe.Pointer) keeps -race/checkptr happy. + L.add( + "\thbox := (*C.uintptr_t)(C.malloc(C.size_t(unsafe.Sizeof(C.uintptr_t(0)))))" + ) + L.add("\t*hbox = C.uintptr_t(h)") + L.add("\tdefer C.free(unsafe.Pointer(hbox))") + L.add( + "\trc := C." & p.procName & + "(n.ctx, C.FFICallBack(C." & cbName & "), unsafe.Pointer(hbox)" & + callArgsStr & ")" + ) + L.add("\tif rc != C.RET_OK {") + L.add("\t\treturn " & retT & "{}, errors.New(\"" & p.procName & ": dispatch failed\")") + L.add("\t}") + L.add("\t<-slot.done") + L.add("\tif slot.err != nil {") + L.add("\t\treturn " & retT & "{}, slot.err") + L.add("\t}") + L.add("\treturn slot.val.(" & retT & "), nil") + L.add("}") + L.add("") + else: + L.add( + "func (n *" & nodeType & ") " & mName & "(" & goParams.join(", ") & + ") (string, error) {" + ) + for c in conv: + L.add(c) + L.add("\tr := C." & libName & "RespNew()") + L.add("\tdefer C." & libName & "RespFree(r)") + L.add("\tC." & libName & "Call_" & p.procName & "(n.ctx" & callArgsStr & ", r)") + L.add("\tif C." & libName & "RespRet(r) != C.RET_OK {") + L.add("\t\treturn \"\", errors.New(respStr(r))") + L.add("\t}") + L.add("\treturn respStr(r), nil") + L.add("}") + L.add("") # ---- destructor ---------------------------------------------------------- if haveDtor: