Skip to content

feat(codegen): propagate {.ffi.} proc doc comments to generated bindings#136

Open
gmelodie wants to merge 1 commit into
masterfrom
feat/propagate-comments
Open

feat(codegen): propagate {.ffi.} proc doc comments to generated bindings#136
gmelodie wants to merge 1 commit into
masterfrom
feat/propagate-comments

Conversation

@gmelodie

@gmelodie gmelodie commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #127. Doc comments written on {.ffi.}-annotated Nim procs were dropped during codegen, so the exported API had to be documented a second time by hand in each target language (or not at all). This carries them through to every generated binding, letting the API be documented once at the source.

The macros lift the proc's leading ## comment out of the AST (extractDocComment, ffi/internal/ffi_macro.nim) into a new doc field on FFIProcMeta / FFIEventMeta, and each generator renders it in its target's native comment syntax.

Given this source:

proc myTimerEcho*(timer: MyTimer, req: EchoRequest): Future[Result[EchoResponse, string]] {.ffi.} =
  ## Sleeps `delayMs` then echoes the message back, firing `on_echo_fired`.

the generated bindings now carry /** Sleeps ... */ on both the exported my_timer_echo symbol and the my_timer_ctx_echo wrapper, /// ... on the C++ echo()/echoAsync() methods and the Rust pub fn echo/echo_async, and a ; comment in the CDDL schema.

What carries a doc

Annotation C (cbor + abi = c) C++ Rust CDDL
{.ffi.} extern decl + _ctx_* wrapper (incl. scalar fast path) method + Async variant extern "C" + pub fn + _async ; rule comment
{.ffiCtor.} extern decl + _ctx_create create + createAsync create + new_async ; rule comment
{.ffiDtor.} extern decl + _ctx_destroy — (rule-of-5 template) — (impl Drop) ; rule comment
{.ffiEvent.} _ctx_add_<ev>_listener addOn<X>Listener add_<ev>_listener

Affected Areas

  • ffi/internal/ffi_macro.nimextractDocComment reads the body's leading nnkCommentStmt; wired into all four registration sites.
  • ffi/codegen/meta.nim — new doc field on FFIProcMeta and FFIEventMeta.
  • ffi/codegen/string_helpers.nimrenderDocComment (line-prefix: ///, ;), renderMemberDocComment (the 4-space class/impl indent) and renderBlockDocComment (C style). All return @[] for an empty doc, so undocumented procs emit byte-identical output to before. */ is escaped to * / so a doc comment cannot close its own block and splice text into the header as code.
  • ffi/codegen/c.nim — both header paths: CBOR (extern decls, ctor, dtor, methods, listeners) and abi = c (extern decls, ctor, dtor, methods, scalar fast path).
  • ffi/codegen/cpp.nim, ffi/codegen/rust.nim — extern decls, ctor, methods, their async variants, and the event-listener API.
  • ffi/codegen/cddl.nim — a ; line under each proc's existing rule header.
  • ffi/codegen/c_cpp_common.nimClassifiedProcs now carries dtor: Option[FFIProcMeta] instead of a bare dtorProcName, so the destructor emitters get the whole meta rather than a widening run of positional strings.
  • examples/ — a few # comments on annotated procs in timer.nim and echo.nim converted to ## so the feature is exercised by the checked-in bindings; the *_bindings/ files are the regenerated output.

Impact on Library Users

Additive and opt-in: a proc with no doc comment generates byte-identical output to before, which the Rust additivity test pins. Adding a ## comment to an annotated proc now changes the generated bindings, so nimble check_bindings will flag them stale until regenerated — worth knowing, since a comment-only edit did not previously affect codegen.

Only ## doc comments on procs propagate. Comments on {.ffi.} types and their fields do not, and cannot without parsing the source text: Nim's parser discards doc comments on nnkTypeDef and object fields before a pragma macro can observe them, and there is no NimNode.comment accessor to recover them. Confirmed against the 2.2.6 AST; documented as a limitation in the README.

Risk Assessment

Low. The change is confined to compile-time metadata and to string emission in codegen — no runtime, wire-format, ABI or memory-management surface is touched, and the doc text never crosses the FFI boundary. The one input that could corrupt a generated C header, a */ inside a doc comment, is escaped and has regression tests both ways.

Testing

  • tests/unit/test_doc_extraction.nim (new, 8 tests) — drives real annotated procs through the macros and asserts on the compile-time registry: single- and multi-line docs, # vs ##, docs alongside a pragma spec, ctor/dtor/event.
  • tests/unit/test_doc_propagation.nim (new, 14 tests) — asserts the exact rendered output per generator, that an undocumented registry leaks no doc text, and that docs are purely additive in the Rust output.
  • tests/unit/test_string_helpers.nim — renderer tests: empty/blank docs, multi-line, indentation, trailing blanks, and the */ escape.
  • Full unit suite passes under --mm:orc and --mm:refc; nimble check_bindings passes for rust, cpp, c and c_abi against the regenerated example bindings.

References

@gmelodie
gmelodie force-pushed the feat/propagate-comments branch from 14cd10a to 301e24b Compare July 16, 2026 20:17
@gmelodie
gmelodie force-pushed the feat/propagate-comments branch from 301e24b to 8776bb8 Compare July 17, 2026 11:56
@gmelodie
gmelodie marked this pull request as ready for review July 17, 2026 11:56
@gmelodie
gmelodie requested a review from Ivansete-status July 17, 2026 17:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Propagate comments from {.ffi.}-annotated Nim through to generated bindings

1 participant