Navigation

C code generation example

This tour builds a small native artifact from a message schema. The example is intentionally compact: one schema file, one hand-written main.c, generated C sources, a static library, and a CLI that proves the generated library links and runs.

The point is to show how a native builder can expose generated files, toolchain steps, descriptors, outputs, and the materialization plan as views of one typed program.

Start from the artifact

The source fixture is c-codegen/messages.def, a tiny key/value schema. The builder generates:

  • include/config.h from typed configuration defines.
  • include/generated_messages.h and generated/generated_messages.c from the schema.
  • lib/libmessages-demo.a from the generated object.
  • bin/messages-demo, linked against the generated library.
  • lib/pkgconfig/messages-demo.pc as metadata for downstream users.

Source files

These pages are the complete source for the worked example: the builder module plus the files it consumes.

  • C codegen builder module - Source for the C codegen builder constructor and worked native demo program.
  • messages.def - Schema fixture consumed by the C code generator in the native demo.
  • main.c - Small CLI source linked against the generated static library.

Builder vocabulary

CCodegenBuilder ornaments BuilderSpec with the native-builder fields that matter here: compiler identity, schema input, config defines, and artifact kind. The public constructor keeps the domain surface focused on the source inputs and leaves the internal program to spell out the build mechanics.

nix
cCodegen {
  name = "messages-demo";
  schema = ./c-codegen/messages.def;
  main = ./c-codegen/main.c;
}

Program walkthrough

The constructor lowers the spec into a sequence of operations: read the schema and main.c, declare bash, gcc, and ar, write the config header, generate C sources, compile the generated source, archive the static library, link the demo CLI, run a smoke test, emit pkg-config metadata, and declare each output.

Because those steps are data, metaBuilder can interpret them without rebuilding the builder by hand. Dependency analysis sees the tool-use edges, dry-run shows the build actions, plan-view exposes the shell plan, and materialization turns the same plan into a derivation.

One build, many views

The section below is generated from mb.program.introspect.run program. It is the internalized view of the native build: validation, dependency graph, dry-run, plan-view, descriptors, outputs, and materialization.

The same internalized program is interpreted several ways.

Validation

  • result: ok

Dependency Graph

  • nodes: 8
  • edges: 5
  • services: 0
  • node tool:bash (tool) package bash-interactive
  • node tool:gcc (tool) package gcc-wrapper
  • node tool:ar (tool) package binutils-wrapper
  • node operation:generate-c-sources (operation) via bash
  • node operation:compile-generated-object (operation) via gcc
  • node operation:archive-library (operation) via ar
  • node operation:link-demo-cli (operation) via gcc
  • node operation:smoke-test (operation) via bash
  • edge tool:bash -> operation:generate-c-sources (uses-tool)
  • edge tool:gcc -> operation:compile-generated-object (uses-tool)
  • edge tool:ar -> operation:archive-library (uses-tool)
  • edge tool:gcc -> operation:link-demo-cli (uses-tool)
  • edge tool:bash -> operation:smoke-test (uses-tool)

Dry Run - Builder

  • source messages.def
  • source main.c
  • tool bash
  • tool gcc
  • tool ar
  • write include/config.h
  • run generate-c-sources with bash
  • run compile-generated-object with gcc
  • run archive-library with ar
  • run link-demo-cli with gcc
  • run smoke-test with bash
  • write lib/pkgconfig/messages-demo.pc
  • descriptor messages-demo-metadata
  • transform cli (elf)
  • transform static-library (archive)
  • transform headers (directory)
  • transform pkg-config (text)
  • materialize messages-demo-artifact with runCommand
  • evidence smoke-test

Dry Run - Runtime

None.

Plan View

  • write write:include/config.h
  • run generate-c-sources
  • run compile-generated-object
  • run archive-library
  • run link-demo-cli
  • run smoke-test
  • write write:lib/pkgconfig/messages-demo.pc
  • write:include/config.h: mkdir -p "$(dirname "$out/include/config.h")"
  • generate-c-sources: bash -c 'set -eu
  • compile-generated-object: gcc -I"$out"/include -c ''"$out"/generated/generated_messages.c -o ''"$out"/generated/generated_messages.o
  • archive-library: ar rcs ''"$out"/lib/libmessages-demo.a ''"$out"/generated/generated_messages.o
  • link-demo-cli: gcc -I"$out"/include "$pathMap_45f70a987ef70685ad13cc6283095c8504a3d4d95f23567a890bb52427d3338d" ''"$out"/lib/libmessages-demo.a -o ''"$out"/bin/messages-demo
  • smoke-test: bash -c 'set -eu
  • write:lib/pkgconfig/messages-demo.pc: mkdir -p "$(dirname "$out/lib/pkgconfig/messages-demo.pc")"

Plan Shell Excerpt

sh
pathMap_301f316e1baa45459961d7b5c3f4a9881e17adfe67b148a8295231c6d567d78f=<source:messages.def>
pathMap_45f70a987ef70685ad13cc6283095c8504a3d4d95f23567a890bb52427d3338d=<source:main.c>
mkdir -p "$(dirname "$out/include/config.h")"
cat > "$out/include/config.h" <<'METABUILDER_HEREDOC_856398558058b5929d6076680e5c3cd22f31adde4a8117afd770ec6dc55cfd7d'
#define DEMO_FEATURE 1
#define MESSAGE_LIMIT 8

METABUILDER_HEREDOC_856398558058b5929d6076680e5c3cd22f31adde4a8117afd770ec6dc55cfd7d
bash -c 'set -eu
schema="$1"

Runtime Services

None.

Descriptors

  • descriptor messages-demo-metadata (payload keys artifactKind, compiler, configDefines, generated, kind)

Builder Self-Documentation

  • documented operations: 19
  • runtime services: 0

Materialized Outputs

  • cli -> $out/bin/messages-demo (elf)
  • static-library -> $out/lib/libmessages-demo.a (archive)
  • headers -> $out/include (directory)
  • pkg-config -> lib/pkgconfig/messages-demo.pc (text)

Materialization

  • derivation: messages-demo-artifact
  • outputs: 4
  • runtime artifacts: 0

Substrates

  • dockerfile: supported
  • shell: supported