allow annotate type with ffi too

This commit is contained in:
Ivan FB 2026-05-03 16:23:00 +02:00
parent d87fe8b104
commit 6c19dba559
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 type NimTimer = object
name: string # set at creation time, read back in each response name: string # set at creation time, read back in each response
ffiType: type TimerConfig {.ffi.} = object
type TimerConfig = object name: string
name: string
ffiType: type EchoRequest {.ffi.} = object
type EchoRequest = object message: string
message: string delayMs: int # how long chronos sleeps before replying
delayMs: int # how long chronos sleeps before replying
ffiType: type EchoResponse {.ffi.} = object
type EchoResponse = object echoed: string
echoed: string timerName: string # proves that the timer's own state is accessible
timerName: string # proves that the timer's own state is accessible
ffiType: type ComplexRequest {.ffi.} = object
type ComplexRequest = object messages: seq[EchoRequest]
messages: seq[EchoRequest] tags: seq[string]
tags: seq[string] note: Option[string]
note: Option[string] retries: Maybe[int]
retries: Maybe[int]
ffiType: type ComplexResponse {.ffi.} = object
type ComplexResponse = object summary: string
summary: string itemCount: int
itemCount: int hasNote: bool
hasNote: bool
# --- Constructor ----------------------------------------------------------- # --- Constructor -----------------------------------------------------------
# Called once from Rust. Creates the FFIContext + NimTimer. # Called once from Rust. Creates the FFIContext + NimTimer.

View File

@ -10,6 +10,31 @@ when defined(ffiGenBindings):
# String helpers used by multiple macros # 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 = proc capitalizeFirstLetter(s: string): string =
## Returns `s` with the first character uppercased. ## Returns `s` with the first character uppercased.
if s.len == 0: if s.len == 0:
@ -560,22 +585,29 @@ macro ffiRaw*(prc: untyped): untyped =
echo result.repr echo result.repr
macro ffi*(prc: untyped): untyped = 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: ## On a type: `type Foo {.ffi.} = object` registers Foo for binding generation
## - Have a first parameter of the library type (e.g. w: Waku) ## and generates ffiSerialize/ffiDeserialize overloads.
## - Optionally have additional Nim-typed parameters
## - Return Future[Result[RetType, string]]
## - NOT include ctx, callback, or userData in its signature
## ##
## 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.} = ## proc mylib_send*(w: MyLib, cfg: SendConfig): Future[Result[string, string]] {.ffi.} =
## return ok("done") ## return ok("done")
##
## The macro generates: if prc.kind == nnkTypeDef:
## 1. A named async helper proc (MyLibSendBody) containing the user body var cleanTypeDef = prc.copyNimTree()
## 2. A registerReqFFI call that deserializes cstring args and calls the helper if cleanTypeDef[0].kind == nnkPragmaExpr:
## 3. A C-exported proc with ctx/callback/userData + cstring params cleanTypeDef[0] = cleanTypeDef[0][0]
return registerFfiTypeInfo(cleanTypeDef)
let procName = prc[0] let procName = prc[0]
let formalParams = prc[3] let formalParams = prc[3]

View File

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