allow annotate type with ffi too

This commit is contained in:
Ivan FB 2026-05-03 16:23:00 +02:00
parent 9524108474
commit 47f3422057
No known key found for this signature in database
GPG Key ID: DF0C67A04C543270
3 changed files with 79 additions and 78 deletions

View File

@ -8,32 +8,27 @@ declareLibrary("nimtimer")
type NimTimer = object
name: string # set at creation time, read back in each response
ffiType:
type TimerConfig = object
name: string
type TimerConfig {.ffi.} = object
name: string
ffiType:
type EchoRequest = object
message: string
delayMs: int # how long chronos sleeps before replying
type EchoRequest {.ffi.} = object
message: string
delayMs: int # how long chronos sleeps before replying
ffiType:
type EchoResponse = object
echoed: string
timerName: string # proves that the timer's own state is accessible
type EchoResponse {.ffi.} = object
echoed: string
timerName: string # proves that the timer's own state is accessible
ffiType:
type ComplexRequest = object
messages: seq[EchoRequest]
tags: seq[string]
note: Option[string]
retries: Maybe[int]
type ComplexRequest {.ffi.} = object
messages: seq[EchoRequest]
tags: seq[string]
note: Option[string]
retries: Maybe[int]
ffiType:
type ComplexResponse = object
summary: string
itemCount: int
hasNote: bool
type ComplexResponse {.ffi.} = object
summary: string
itemCount: int
hasNote: bool
# --- Constructor -----------------------------------------------------------
# Called once from Rust. Creates the FFIContext + NimTimer.

View File

@ -10,6 +10,31 @@ when defined(ffiGenBindings):
# String helpers used by multiple macros
# ---------------------------------------------------------------------------
proc registerFfiTypeInfo(typeDef: NimNode): NimNode {.compileTime.} =
## Registers the type in ffiTypeRegistry for binding generation and returns
## the clean typeDef. Serialization is handled by the generic overloads in serial.nim.
let typeName =
if typeDef[0].kind == nnkPostfix: typeDef[0][1] else: typeDef[0]
let typeNameStr = $typeName
var fieldMetas: seq[FFIFieldMeta] = @[]
let objTy = typeDef[2]
if objTy.kind == nnkObjectTy and objTy.len >= 3:
let recList = objTy[2]
if recList.kind == nnkRecList:
for identDef in recList:
if identDef.kind == nnkIdentDefs:
let fieldType = identDef[^2]
let fieldTypeName =
if fieldType.kind == nnkIdent: $fieldType
elif fieldType.kind == nnkPtrTy: "ptr " & $fieldType[0]
else: fieldType.repr
for i in 0 ..< identDef.len - 2:
fieldMetas.add(FFIFieldMeta(name: $identDef[i], typeName: fieldTypeName))
ffiTypeRegistry.add(FFITypeMeta(name: typeNameStr, fields: fieldMetas))
result = typeDef
proc capitalizeFirstLetter(s: string): string =
## Returns `s` with the first character uppercased.
if s.len == 0:
@ -560,22 +585,29 @@ macro ffiRaw*(prc: untyped): untyped =
echo result.repr
macro ffi*(prc: untyped): untyped =
## Simplified FFI macro — developer writes a clean Nim-idiomatic signature.
## Simplified FFI macro — applies to procs or types.
##
## The annotated proc must:
## - Have a first parameter of the library type (e.g. w: Waku)
## - Optionally have additional Nim-typed parameters
## - Return Future[Result[RetType, string]]
## - NOT include ctx, callback, or userData in its signature
## On a type: `type Foo {.ffi.} = object` registers Foo for binding generation
## and generates ffiSerialize/ffiDeserialize overloads.
##
## Example:
## On a proc: the annotated proc must have a first parameter of the library type,
## optionally additional Nim-typed parameters, and return Future[Result[RetType, string]].
## It must NOT include ctx, callback, or userData in its signature.
##
## Example (type):
## type EchoRequest {.ffi.} = object
## message: string
## delayMs: int
##
## Example (proc):
## proc mylib_send*(w: MyLib, cfg: SendConfig): Future[Result[string, string]] {.ffi.} =
## return ok("done")
##
## The macro generates:
## 1. A named async helper proc (MyLibSendBody) containing the user body
## 2. A registerReqFFI call that deserializes cstring args and calls the helper
## 3. A C-exported proc with ctx/callback/userData + cstring params
if prc.kind == nnkTypeDef:
var cleanTypeDef = prc.copyNimTree()
if cleanTypeDef[0].kind == nnkPragmaExpr:
cleanTypeDef[0] = cleanTypeDef[0][0]
return registerFfiTypeInfo(cleanTypeDef)
let procName = prc[0]
let formalParams = prc[3]

View File

@ -1,4 +1,4 @@
import std/[json, macros, sequtils, options]
import std/[json, macros, options]
import results
import ./codegen/meta
@ -81,6 +81,9 @@ proc ffiSerialize*[T](x: Option[T]): string =
else:
"null"
proc ffiSerialize*[T: object](x: T): string =
$(%*x)
proc ffiDeserialize*[T](s: cstring, _: typedesc[ptr T]): Result[ptr T, string] =
try:
let address = cast[ptr T](uint(parseJson($s).getBiggestInt()))
@ -120,10 +123,16 @@ proc ffiDeserialize*[T](s: cstring, _: typedesc[Option[T]]): Result[Option[T], s
except Exception as e:
err(e.msg)
proc ffiDeserialize*[T: object](s: cstring, _: typedesc[T]): Result[T, string] =
try:
ok(parseJson($s).to(T))
except Exception as e:
err(e.msg)
macro ffiType*(body: untyped): untyped =
## Statement macro applied to a type declaration block.
## Generates ffiSerialize and ffiDeserialize overloads for each type,
## and registers the type in ffiTypeRegistry for binding generation.
## Registers the type in ffiTypeRegistry for binding generation.
## Serialization is handled by the generic ffiSerialize/ffiDeserialize overloads.
## Usage:
## ffiType:
## type Foo = object
@ -136,56 +145,21 @@ macro ffiType*(body: untyped): untyped =
else:
typeDef[0]
# Collect field metadata for the codegen registry
let typeNameStr = $typeName
var fieldMetas: seq[FFIFieldMeta] = @[]
# typeDef layout: TypDef[name, genericParams, objectTy]
# objectTy layout: ObjectTy[empty, empty, recList]
let objTy = typeDef[2]
if objTy.kind == nnkObjectTy and objTy.len >= 3:
let recList = objTy[2]
if recList.kind == nnkRecList:
for identDef in recList:
if identDef.kind == nnkIdentDefs:
# identDef: [name1, ..., type, default]
let fieldType = identDef[^2]
var fieldTypeName: string
if fieldType.kind == nnkIdent:
fieldTypeName = $fieldType
elif fieldType.kind == nnkPtrTy:
fieldTypeName = "ptr " & $fieldType[0]
else:
fieldTypeName = fieldType.repr
let fieldTypeName =
if fieldType.kind == nnkIdent: $fieldType
elif fieldType.kind == nnkPtrTy: "ptr " & $fieldType[0]
else: fieldType.repr
for i in 0 ..< identDef.len - 2:
let fname = $identDef[i]
fieldMetas.add(FFIFieldMeta(name: fname, typeName: fieldTypeName))
fieldMetas.add(FFIFieldMeta(name: $identDef[i], typeName: fieldTypeName))
ffiTypeRegistry.add(FFITypeMeta(name: typeNameStr, fields: fieldMetas))
let serializeProc = quote:
proc ffiSerialize*(x: `typeName`): string =
$(%*x)
var assignmentText = ""
for field in fieldMetas:
if assignmentText.len > 0:
assignmentText &= "\n"
assignmentText &=
" result[\"" & field.name & "\"] = parseJson(ffiSerialize(x." & field.name & "))"
let jsonProc = parseStmt(
"proc `%`*(x: " & typeNameStr & "): JsonNode =\n var result = newJObject()\n" &
assignmentText & "\n return result\n"
)
let importJson = quote:
import json
let deserializeProc = quote:
proc ffiDeserialize*(
s: cstring, _: typedesc[`typeName`]
): Result[`typeName`, string] =
try:
ok(parseJson($s).to(`typeName`))
except Exception as e:
err(e.msg)
result = newStmtList(importJson, body, serializeProc, jsonProc, deserializeProc)
result = body