From 47f3422057913e883b32136c8b2dbe0e7ad92c9d Mon Sep 17 00:00:00 2001 From: Ivan FB Date: Sun, 3 May 2026 16:23:00 +0200 Subject: [PATCH] allow annotate type with ffi too --- examples/nim_timer/nim_timer.nim | 39 +++++++++----------- ffi/internal/ffi_macro.nim | 56 ++++++++++++++++++++++------- ffi/serial.nim | 62 ++++++++++---------------------- 3 files changed, 79 insertions(+), 78 deletions(-) diff --git a/examples/nim_timer/nim_timer.nim b/examples/nim_timer/nim_timer.nim index 4df4391..a9f0329 100644 --- a/examples/nim_timer/nim_timer.nim +++ b/examples/nim_timer/nim_timer.nim @@ -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. diff --git a/ffi/internal/ffi_macro.nim b/ffi/internal/ffi_macro.nim index 9be68da..fadbcee 100644 --- a/ffi/internal/ffi_macro.nim +++ b/ffi/internal/ffi_macro.nim @@ -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] diff --git a/ffi/serial.nim b/ffi/serial.nim index 935db4b..d0f3cf8 100644 --- a/ffi/serial.nim +++ b/ffi/serial.nim @@ -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