nim-ffi/ffi/internal/ffi_macro.nim
Ivan Folgueira Bande 5964e287a9
rm useless comments
2025-12-11 17:21:47 +01:00

417 lines
12 KiB
Nim

import std/[macros, tables]
import chronos
import ../ffi_types
proc extractFieldsFromLambda(body: NimNode): seq[NimNode] =
## Extracts the fields (params) from the given lambda body.
var procNode = body
if procNode.kind == nnkStmtList and procNode.len == 1:
procNode = procNode[0]
if procNode.kind != nnkLambda and procNode.kind != nnkProcDef:
error "registerReqFFI expects a lambda proc, found: " & $procNode.kind
let params = procNode[3] # parameters list
result = @[]
for p in params[1 .. ^1]: # skip return type
result.add newIdentDefs(p[0], p[1])
proc buildRequestType(reqTypeName: NimNode, body: NimNode): NimNode =
## Builds:
## type <reqTypeName>* = object
## <lambdaParam1Name>: <lambdaParam1Type>
## ...
var procNode = body
if procNode.kind == nnkStmtList and procNode.len == 1:
procNode = procNode[0]
if procNode.kind != nnkLambda and procNode.kind != nnkProcDef:
error "registerReqFFI expects a lambda proc, found: " & $procNode.kind
let params = procNode[3] # formal params of the lambda
var fields: seq[NimNode] = @[]
for p in params[1 .. ^1]: # skip return type at index 0
let name = p[0]
let typ = p[1]
# Field must be nnkIdentDefs(name, type, defaultExpr)
fields.add newTree(nnkIdentDefs, name, typ, newEmptyNode())
# Wrap fields in a rec list
let recList = newTree(nnkRecList, fields)
# object type node: object [of?] [] [pragma?] recList
let objTy = newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), recList)
# Export the type (CreateNodeRequest*)
let typeName =
if reqTypeName.kind == nnkPostfix:
reqTypeName
else:
postfix(reqTypeName, "*")
result =
newNimNode(nnkTypeSection).add(newTree(nnkTypeDef, typeName, newEmptyNode(), objTy))
proc buildFfiNewReqProc(reqTypeName, body: NimNode): NimNode =
var formalParams = newSeq[NimNode]()
var procNode: NimNode
if body.kind == nnkStmtList and body.len == 1:
procNode = body[0] # unwrap single statement
else:
procNode = body
if procNode.kind != nnkLambda and procNode.kind != nnkProcDef:
error "registerReqFFI expects a lambda definition. Found: " & $procNode.kind
# T: typedesc[CreateNodeRequest]
let typedescParam = newIdentDefs(
ident("T"), # param name
nnkBracketExpr.newTree(ident("typedesc"), reqTypeName), # typedesc[T]
)
formalParams.add(typedescParam)
# Other fixed FFI params
formalParams.add(newIdentDefs(ident("callback"), ident("FFICallBack")))
formalParams.add(newIdentDefs(ident("userData"), ident("pointer")))
# Add original lambda params
let procParams = procNode[3]
for p in procParams[1 .. ^1]:
formalParams.add(p)
# Build `ptr FFIThreadRequest`
let retType = newNimNode(nnkPtrTy)
retType.add(ident("FFIThreadRequest"))
formalParams = @[retType] & formalParams
# Build body
let reqObjIdent = ident("reqObj")
var newBody = newStmtList()
newBody.add(
quote do:
var `reqObjIdent` = createShared(T)
)
for p in procParams[1 .. ^1]:
let fieldNameIdent = ident($p[0])
let fieldTypeNode = p[1]
# Extract type name as string
var typeStr: string
if fieldTypeNode.kind == nnkIdent:
typeStr = $fieldTypeNode
elif fieldTypeNode.kind == nnkBracketExpr:
typeStr = $fieldTypeNode[0] # e.g., `ptr` in `ptr[Waku]`
else:
typeStr = "" # fallback
# Apply .alloc() only to cstrings
if typeStr == "cstring":
newBody.add(
quote do:
`reqObjIdent`[].`fieldNameIdent` = `fieldNameIdent`.alloc()
)
else:
newBody.add(
quote do:
`reqObjIdent`[].`fieldNameIdent` = `fieldNameIdent`
)
# FFIThreadRequest.init using fnv1aHash32
newBody.add(
quote do:
let typeStr = $T
var ret =
FFIThreadRequest.init(callback, userData, typeStr.cstring, `reqObjIdent`)
return ret
)
# Build the proc node
result = newProc(
name = postfix(ident("ffiNewReq"), "*"),
params = formalParams,
body = newBody,
pragmas = newEmptyNode(),
)
proc buildFfiDeleteReqProc(reqTypeName: NimNode, fields: seq[NimNode]): NimNode =
## Generates:
## proc ffiDeleteReq(self: ptr <reqTypeName>) =
## deallocShared(self[].<cstringField>)
## deallocShared(self)
# Build the body
var body = newStmtList()
for f in fields:
if $f[1] == "cstring": # only dealloc cstring fields
body.add newCall(
ident("deallocShared"),
newDotExpr(newTree(nnkDerefExpr, ident("self")), ident($f[0])),
)
# Always free the whole object at the end
body.add newCall(ident("deallocShared"), ident("self"))
# Build the parameter: (self: ptr <reqTypeName>)
let selfParam = newIdentDefs(ident("self"), newTree(nnkPtrTy, reqTypeName))
# Build the proc definition
result = newProc(
name = postfix(ident("ffiDeleteReq"), "*"),
params = @[newEmptyNode()] & @[selfParam], # ✅ properly wrapped in a sequence
body = body,
)
proc buildProcessFFIRequestProc(reqTypeName, reqHandler, body: NimNode): NimNode =
## Builds:
## proc processFFIRequest(T: typedesc[CreateNodeRequest];
## configJson: cstring;
## appCallbacks: AppCallbacks;
## waku: ptr Waku) ...
## Builds:
## proc processFFIRequest*(request: pointer; waku: ptr Waku) ...
if reqHandler.kind != nnkExprColonExpr:
error(
"Second argument must be a typed parameter, e.g., waku: ptr Waku. Found: " &
$reqHandler.kind
)
let rhs = reqHandler[1]
if rhs.kind != nnkPtrTy:
error("Second argument must be a pointer type, e.g., waku: ptr Waku")
var procNode = body
if procNode.kind == nnkStmtList and procNode.len == 1:
procNode = procNode[0]
if procNode.kind != nnkLambda and procNode.kind != nnkProcDef:
error "registerReqFFI expects a lambda definition. Found: " & $procNode.kind
let typedescParam =
newIdentDefs(ident("T"), nnkBracketExpr.newTree(ident("typedesc"), reqTypeName))
# Build formal params: (returnType, request: pointer, waku: ptr Waku)
let procParams = procNode[3]
var formalParams: seq[NimNode] = @[]
formalParams.add(procParams[0]) # return type
formalParams.add(typedescParam)
formalParams.add(newIdentDefs(ident("request"), ident("pointer")))
formalParams.add(newIdentDefs(reqHandler[0], rhs)) # e.g. waku: ptr Waku
# Inject cast/unpack/defer into the body
let bodyNode =
if procNode.body.kind == nnkStmtList:
procNode.body
else:
newStmtList(procNode.body)
let newBody = newStmtList()
let reqIdent = ident("req")
newBody.add quote do:
let `reqIdent`: ptr `reqTypeName` = cast[ptr `reqTypeName`](request)
defer:
ffiDeleteReq(`reqIdent`)
# automatically unpack fields into locals
for p in procParams[1 ..^ 1]:
let fieldName = p[0] # Ident
newBody.add quote do:
let `fieldName` = `reqIdent`[].`fieldName`
# Append user's lambda body
newBody.add(bodyNode)
result = newProc(
name = postfix(ident("processFFIRequest"), "*"),
params = formalParams,
body = newBody,
procType = nnkProcDef,
pragmas =
if procNode.len >= 5:
procNode[4]
else:
newEmptyNode(),
)
proc addNewRequestToRegistry(reqTypeName, reqHandler: NimNode): NimNode =
## Adds a new request to the registeredRequests table.
## The key is the hash of the request type name, and the value is the NimNode
## representing the request type.
# Build: request[].reqContent
let reqContent =
newDotExpr(newTree(nnkDerefExpr, ident("request")), ident("reqContent"))
# Build Future[Result[string, string]] return type
let returnType = nnkBracketExpr.newTree(
ident("Future"),
nnkBracketExpr.newTree(ident("Result"), ident("string"), ident("string")),
)
# Extract the type from reqHandler (generic: ptr Waku, ptr Foo, ptr Bar, etc.)
let rhsType =
if reqHandler.kind == nnkExprColonExpr:
reqHandler[1] # Use the explicit type
else:
error "Second argument must be a typed parameter, e.g. waku: ptr Waku"
# Build: cast[ptr Waku](reqHandler) or cast[ptr Foo](reqHandler) dynamically
let castedHandler = newTree(
nnkCast,
rhsType, # The type, e.g. ptr Waku
ident("reqHandler"), # The expression to cast
)
let callExpr = newCall(
newDotExpr(reqTypeName, ident("processFFIRequest")), ident("request"), castedHandler
)
var newBody = newStmtList()
newBody.add(
quote do:
return await `callExpr`
)
# Build:
# proc(request: pointer, reqHandler: pointer):
# Future[Result[string, string]] {.async.} =
# CreateNodeRequest.processFFIRequest(request, reqHandler)
let asyncProc = newProc(
name = newEmptyNode(), # anonymous proc
params =
@[
returnType,
newIdentDefs(ident("request"), ident("pointer")),
newIdentDefs(ident("reqHandler"), ident("pointer")),
],
body = newBody,
pragmas = nnkPragma.newTree(ident("async")),
)
let reqTypeNameStr = $reqTypeName
# Use a string literal instead of reqTypeNameStr
let key = newLit($reqTypeName)
# Generate: registeredRequests["CreateNodeRequest"] = <generated proc>
result =
newAssignment(newTree(nnkBracketExpr, ident("registeredRequests"), key), asyncProc)
macro registerReqFFI*(reqTypeName, reqHandler, body: untyped): untyped =
## Registers a request that will be handled by the ffi thread.
## The request should be sent from the ffi consumer thread.
##
# Extract lambda params to generate fields
let fields = extractFieldsFromLambda(body)
let typeDef = buildRequestType(reqTypeName, body)
let ffiNewReqProc = buildFfiNewReqProc(reqTypeName, body)
let processProc = buildProcessFFIRequestProc(reqTypeName, reqHandler, body)
let addNewReqToReg = addNewRequestToRegistry(reqTypeName, reqHandler)
let deleteProc = buildFfiDeleteReqProc(reqTypeName, fields)
result = newStmtList(typeDef, ffiNewReqProc, deleteProc, processProc, addNewReqToReg)
macro processReq*(
reqType, ctx, callback, userData: untyped, args: varargs[untyped]
): untyped =
## Expands T.processReq(ctx, callback, userData, a, b, ...)
var callArgs = @[reqType, callback, userData]
for a in args:
callArgs.add a
let newReqCall = newCall(ident("ffiNewReq"), callArgs)
let sendCall = newCall(
newDotExpr(ident("ffi_context"), ident("sendRequestToFFIThread")), ctx, newReqCall
)
result = quote:
block:
let res = `sendCall`
if res.isErr():
let msg = "error in sendRequestToFFIThread: " & res.error
`callback`(RET_ERR, unsafeAddr msg[0], cast[csize_t](msg.len), `userData`)
return RET_ERR
return RET_OK
macro ffi*(prc: untyped): untyped =
let procName = prc[0]
let formalParams = prc[3]
let bodyNode = prc[^1]
if formalParams.len < 2:
error("`.ffi.` procs require at least 1 parameter")
let firstParam = formalParams[1]
let paramIdent = firstParam[0]
let paramType = firstParam[1]
let reqName = ident($procName & "Req")
let returnType = ident("cint")
# Build parameter list (skip return type)
var newParams = newSeq[NimNode]()
newParams.add(returnType)
for i in 1 ..< formalParams.len:
newParams.add(newIdentDefs(formalParams[i][0], formalParams[i][1]))
# Build Future[Result[string, string]] return type
let futReturnType = quote:
Future[Result[string, string]]
var userParams = newSeq[NimNode]()
userParams.add(futReturnType)
if formalParams.len > 3:
for i in 4 ..< formalParams.len:
userParams.add(newIdentDefs(formalParams[i][0], formalParams[i][1]))
# Build argument list for processReq
var argsList = newSeq[NimNode]()
for i in 1 ..< formalParams.len:
argsList.add(formalParams[i][0])
# 1. Build the dot expression. e.g.: waku_is_onlineReq.processReq
let dotExpr = newTree(nnkDotExpr, reqName, ident"processReq")
# 2. Build the call node with dotExpr as callee
let callNode = newTree(nnkCall, dotExpr)
for arg in argsList:
callNode.add(arg)
# Proc body
let ffiBody = newStmtList(
quote do:
initializeLibrary()
if not isNil(ctx):
ctx[].userData = userData
if isNil(callback):
return RET_MISSING_CALLBACK
)
ffiBody.add(callNode)
let ffiProc = newProc(
name = procName,
params = newParams,
body = ffiBody,
pragmas = newTree(nnkPragma, ident "dynlib", ident "exportc", ident "cdecl"),
)
var anonymousProcNode = newProc(
name = newEmptyNode(), # anonymous proc
params = userParams,
body = newStmtList(bodyNode),
pragmas = newTree(nnkPragma, ident"async"),
)
# registerReqFFI wrapper
let registerReq = quote:
registerReqFFI(`reqName`, `paramIdent`: `paramType`):
`anonymousProcNode`
result = newStmtList(registerReq, ffiProc)