Skip to content
helia-ui
Sections
HELIA HUB

C and C++ API reference

C and C++ projects document their headers with Doxygen, which generates its own HTML and has no Starlight equivalent. helia-ui-doxyref is the replacement, and it is the Python generator’s twin. An extractor maps Doxygen’s XML onto the reference model; the same renderer the Python reference goes through turns that model into MDX pages that compose the package’s RefSymbol, RefParams, RefSection and RefMembers parts, into the JSON the model is, and into the two text artifacts an agent reads instead of the pages.

That shared renderer is the whole point. Only scripts/lib/doxyref-extract.mjs knows C. A C reference and a Python reference are the same pages with different content, not two designs that drift.

The generator never compiles the code it documents. Doxygen parses, so a firmware header that only builds for a Cortex-M target documents on any machine, and the site build needs no toolchain at all.

The JSON is the source of truth and the pages are a view of it.

The model is written out whole and per module, with sorted keys and no timestamp, so it diffs. Every symbol keeps a stable id, and that id is the page anchor, so a URL plus a fragment names one symbol and keeps naming it after the reference is regenerated or restyled. A consumer that wants the data takes the JSON and never parses HTML.

The model is @ambiqai/helia-ui/reference-model: TypeScript types plus validateReferenceModel, a hand-written guard with no dependencies. It is the same model the Python extractor produces, which is why direction exists on a parameter at all — C is the language that needs it.

Two settings, on top of whatever the project already sets:

GENERATE_XML = YES
XML_PROGRAMLISTING = NO

GENERATE_XML is what produces the input. XML_PROGRAMLISTING copies every documented file’s full source into the XML; the generator does not read it, and leaving it on multiplies the dump size for nothing. A @code block inside a comment is unaffected and still becomes a fenced block on the page.

OPTIMIZE_OUTPUT_FOR_C = YES is worth setting for a C project, and EXTRACT_ALL = YES decides whether undocumented declarations appear. Both are the project’s call: the generator publishes what Doxygen emitted.

Terminal window
# In the C repository:
doxygen Doxyfile
# In the docs site:
npx helia-ui-doxyref \
--xml ../build/xml \
--out src/content/docs/reference/api \
--public public \
--base /helia-sdk/ \
--sidebar src/generated/api-sidebar.json
Command-line options
OptionDefaultWhat it does
--xml <dir>requiredDoxygen's XML_OUTPUT directory.
--out <dir>requiredDirectory the module pages are written into.
--public <dir>publicDirectory the JSON and text artifacts are written into.
--base <path>/Site base path, prefixed onto every URL the pages and artifacts carry.
--site <origin>not setOrigin for the absolute URLs in llms.txt. Without it they are site-root relative.
--name <library>Doxygen's PROJECT_NAMEThe library the reference documents. It names the root page and the route under it.
--language <c|cpp>read from the dumpOverride the language on every symbol.
--sidebar <file>not writtenWrite the Starlight sidebar fragment as JSON.
--source-root <dir>not setStrip this prefix from the source paths in the model.
--source-url <template>not setLink template for source, with {path} and {line}.
--commit <sha>not setCommit of the documented source, recorded in generatedFrom.
--checkoffCompare against what is on disk and exit 1 on drift.
--quietoffReport only the warning count, not each warning.
--route-prefix <path>reference/apiRoute the pages serve under.
--heading-level <n>2Rank of a symbol heading.
--show-root-headingoffRepeat the module's name as a heading.
--no-show-signatureoffLeave the declaration off every symbol.

Doxygen’s tree is compounds holding memberdef entries. The extractor folds that onto the model once, here:

Doxygen compounds and members in the reference model
DoxygenModelNotes
compound filemoduleOne page per header. The route drops the extension so it splits like every other module path; the title keeps it.
compound namespace, groupmoduleA member a group claims is not repeated on the page for the file that declares it.
compound struct, class, union, interfacesymbolOn the module that declares it, with its fields and methods nested under it.
compound dir, page, exampledroppedNavigation, not API surface.
memberdef functionfunctionA method when it sits inside a class.
memberdef variableattributeA struct field or a global.
memberdef definemacroRendered as the #define, value included.
memberdef typedeftypeSame kind a TypeScript alias takes.
memberdef enumenumIts enumvalue children become members.
enumvalueconstant

Doxygen’s commands become fields rather than prose, so the renderer draws them as the same tables every other language gets.

Doxygen commands and the fields they become
CommandField
@param, with [in], [out] or [in,out]params, with direction
@return, @returnsreturns
@retvalreturns, named
@throws, @exceptionraises
@sincesince
@deprecateddeprecated
@briefsummary, and the page description
@note, @warning, @see, @paran aside in the description

A declaration is the authority on which parameters exist and how they are typed; the comment is the authority on what they mean. A @param naming an argument the declaration does not have is reported as a warning rather than rendered as a row nobody can pass.

Brief and detailed descriptions are rendered to Markdown: paragraphs, @code blocks as fenced blocks in the symbol’s language, lists, tables, links and emphasis. A ref — Doxygen’s cross-reference to another documented symbol — becomes the identifier in a code span, because Doxygen’s ids name its own output files rather than model symbols, and the renderer owns routes.

Cross-references written in the model’s own syntax, [Name][symbol_id], do resolve: the renderer turns them into a link to the page and anchor that carries the symbol, prefixed with --base.

Doxygen reports a .h file as C++ whatever is in it, so the language attribute alone would label every C project C++. A dump with no class and no namespace, out of files that are all C translation units or headers, is read as C. --language settles it either way, and the value reaches the page as the chip beside each symbol and the grammar its declaration is highlighted with.

Alongside the pages, a run writes four kinds of file under <public>/<route prefix>/: reference.json, one <module path>.json per module, llms.txt and llms-full.txt. They are what the Python generator writes, in the same places, because they are the same renderer. The Python reference page describes them in full.

Generated pages and artifacts are committed, so the site builds without Doxygen. --check renders in memory and compares against what is on disk, exiting 1 on drift and naming every file that is stale, missing or orphaned.

- run: doxygen Doxyfile
- run: npx helia-ui-doxyref --xml build/xml --out docs/src/content/docs/reference/api --public docs/public --base /helia-sdk/ --check

Put it in the same workflow that builds the docs. A comment edited without regenerating the reference then fails the pull request rather than shipping a page that disagrees with the header.

A run records what it wrote in <out>/.doxyref-manifest.json and removes only those files next time, so a hand-written index page kept in the same directory survives.

Doxygen’s XML is a schema’d format that ships its own compound.xsd, so the extractor takes the version from index.xml and records it in generatedFrom rather than pinning one release. It was developed against Doxygen 1.17.0, and the fixture its tests run on is that release’s own output rather than a hand-written approximation, so a change in the format fails a test instead of a product site.

The XML is read by a small reader in the package. Adding a parser dependency would have meant a product repository installing one to build its docs, and these generators run with nothing installed but this package.