Templates
The c code DDD generates is rendered from templates the project provides. ddd generate
therefore takes a required -t/--template-dir: there is no built-in default and there is
no fallback, so a run that leaves the option out is a usage mistake rather than a run that
silently produces somebody else’s idea of c code.
$ ddd generate examples/demo/demo.ddd.json -o build/gen
usage: ddd generate [-h] [-W CHECK=SEVERITY] [--strict] [--format {text,json}]
-o OUTPUT_DIR -t TEMPLATE_DIR [--const-inputs] [--no-a2l]
[--byte-order {little,big}] [--address-map ADDRESS_MAP]
[--dry-run] [--force]
project
ddd generate: error: the following arguments are required: -t/--template-dir
Why the templates belong to the project
What a variable is called, what type it has, which component owns it and which components
read it - all of that follows from the description files, and DDD resolves and checks it.
None of what a c file looks like follows from it. Whether the file opens with a banner and
what that banner says, what a comment above a declaration looks like, whether the
include guard is DDD_GLOBALS_H or a name your coding standard derives from the module
number, whether the file is called ddd_globals.c or proj_data.c, in which order the
sections appear, how deep the indentation is - every one of those is a house style, settled
per project, usually by a coding standard that is older than DDD and applies to the
hand-written code as well.
A generator that decided them would impose the habits of the project it was first written
for on every project after it. The consequence is familiar: either the generated files are
patched after each run, which makes regeneration unsafe, or the generator grows one option
per habit until it carries a flag for the guard, one for the comment style, one for the
banner text and one for each file name. DDD instead supplies the resolved data and lets the
project write the file, so a project that wants a particular documentation convention writes
it, a project that wants its MISRA deviation record above every volatile writes that, and
neither has to argue
with the tool or with the other.
What stays with DDD is everything that is not a matter of taste: reading and validating the descriptions, resolving producers against consumers, computing shapes and limits, ordering the objects so that a diff stays readable, and defusing any text that ends up inside a comment. The templates only spell out the result. This is also why there is no option for naming the generated files: a project renames one by renaming its template, which is one mechanism instead of a second one bolted on top of it.
The a2l file is not templated
Only the c backend takes a template directory. The a2l generator is internal and has no template option at all, because the structure of an ASAP2 file is dictated by ASAM rather than by a project: its readers are measurement and calibration tools that implement the standard, and a house style in that file is not a preference, it is a file the tool refuses. The distinction is the whole argument on this page in one line - the c code is read by your developers, so your project decides how it reads; the a2l file is read by a program, so the standard decides. See generated artefacts for what the a2l backend produces.
Why jinja2
The templates are jinja2 templates. It is the template engine most people who will edit them have already met, it is documented well enough that nobody has to read DDD’s source to use it, and it is text-oriented rather than markup-oriented: a template is the c file you want, with holes in it, which is what makes a template reviewable by the same people who review the c code.
The environment DDD renders in is set up for source files rather than for web pages.
Autoescaping is off, since escaping html entities into c would corrupt it; trim_blocks
and lstrip_blocks are on, so a {% for %} on a line of its own does not leave a blank
line and an indented {% if %} does not leave its indentation behind; the trailing newline
of a template is kept, and a rendered file that does not end in one gets one added.
Undefined values are strict, which is the subject of a section of its own below.
How the template directory is read
Four rules decide what is rendered and what the result is called. They are deliberately
mechanical, because a build system has to derive the set of generated files from the template
directory alone, without running the tool first - which is what lets ddd_generate in the
cmake integration declare the outputs of the generation step
before it has ever run.
Every
*.jinja2file directly inside the template directory is rendered.The generated file is named like its template without the
.jinja2extension, soddd_globals.c.jinja2producesddd_globals.c. The template a given file came out of is therefore always identifiable from the file name alone.A template whose name starts with an underscore is a helper: it produces no file of its own and exists to be imported by the others.
A template whose name contains
{component}is rendered once per component, with the placeholder replaced by the component name.
The example directory shipped with DDD contains one template of each kind:
templates/
_macros.jinja2
ddd_globals.c.jinja2
ddd_globals.h.jinja2
ddd_types.h.jinja2
{component}.h.jinja2
Applied to the demonstration project in examples/demo, which is made of the four
components Controller, SensorHub, UserInterface and EventLogger, those five
templates produce the following:
template |
generated file |
|---|---|
|
nothing - a helper, imported by the four others |
|
|
|
|
|
|
|
|
$ ddd generate examples/demo/demo.ddd.json -o build/gen -t examples/templates
wrote build/gen/ddd_globals.c (created)
wrote build/gen/ddd_globals.h (created)
wrote build/gen/ddd_types.h (created)
wrote build/gen/Controller.h (created)
wrote build/gen/SensorHub.h (created)
wrote build/gen/UserInterface.h (created)
wrote build/gen/EventLogger.h (created)
wrote build/gen/DemoDevice.a2l (created)
The a2l file at the end comes from the other backend and is unaffected by the templates. A directory in which nothing is left to render - one holding only helpers, for instance - is refused with a message saying so, rather than quietly producing no c code at all.
What a template receives
A template is rendered with a small, fixed context, and everything else is reached by
traversing it. All of it is a c-shaped view of the resolved data dictionary - the same data ddd dump publishes, with the resolving already
done: producers matched, limits derived, conditions validated, objects ordered. A template
reads answers, it never computes them; a tool that wants the data without the c slant
reads the dictionary itself.
modelThe whole project, prepared for c. Always present.
filenameThe name of the file this rendering produces, relative to the output directory:
ddd_globals.c, orController.hfor a per-component rendering. It is passed in rather than left to the template to reconstruct, so that a banner can print the name of the file it is in without knowing which of the two rules produced it.headerPresent only in a
{component}template: the component being rendered. A template without the placeholder never sees it, which is what makes the mistake of iterating over the components inside a per-component template fail immediately instead of silently writing every component into every file.
The model
model carries the project as flat sequences, already ordered and already safe to print:
attribute |
what it holds |
|---|---|
|
The project name, or the component name when a single component is generated. |
|
The name of the description file the dictionary was read from, for the banner. |
|
The tool and version that produced the file, such as |
|
One entry per declared constant, in name order, each
with |
|
One entry per enum conversion, each with |
|
The objects to be defined, grouped by owning component: |
|
The placed objects grouped per linker section, one
entry per section with |
|
One entry per component, in project order, whether or not the component declares anything. These are the interfaces, described below. |
|
Whether any datatype of the project needs that standard header, so that the generated type header includes it only when something uses it. |
|
The headers of the external types in use, deduplicated and
sorted by spelling, each ready to paste after |
|
A normalised include guard, e.g. |
|
What the command line let the caller decide, which for the c backend is
|
The two groupings answer two different questions, which is why both exist. model.groups
is about ownership and drives the file that allocates the memory; model.headers is
about visibility and drives the file each component includes.
A variable
The entries of .measurements, .calibration and .variables are the objects
themselves:
attribute |
what it holds |
|---|---|
|
The object name, exactly as it appears in the description. |
|
The complete definition without the trailing semicolon, qualifier, type, declarator
and initialiser included: |
|
The matching |
|
The description, the unit in square brackets and, for calibration data, a note saying
what the object is and what it is dimensioned by - or nothing at all if the
description says nothing. It is plain text without comment markers, already collapsed
to one line and already defused, so that a |
|
The preprocessor condition the object is guarded by, or nothing. It is validated on
the way in so that it is safe to emit verbatim into both |
|
The name of the linker section the producing
declaration placed the object in - the name alone, as the linker script spells it -
or nothing when the object is unplaced. How a placement is spelled in c, an
|
|
The component that produces the object and the components that read it, for a comment that says where a value comes from. |
.kind, .c_type, .array_suffix, .qualifier and .initializer are there as
well, for a template that would rather lay the declaration out itself than take
.definition whole. .array_suffix spells each dimension as the project spells it - a
dimension stated as a declared constant renders as that
name, [PRESSURE_CELLS], in .definition and .declaration(...) alike - while
.initializer lays its braces out over the resolved numeric shape. .qualifier is derived rather than stored, from the two answers a
declaration can give about who writes the object: .constant is true for calibration data,
which the software never writes and which is therefore generated const, and .volatile
is what the description states on the definition, on every kind, to say that something
outside the compiled code - an interrupt, another core, a calibration tool - changes the
value while it runs. Either, both or neither may hold, and .qualifier is simply the one
or two keywords that follow, with their trailing space. A template that composes its own
declaration is better off testing the two booleans than matching text against the string they
produce, and they are also what a MISRA deviation record above every volatile object, or
a section attribute on the tunable data alone, is written from.
A component header
header, and every entry of model.headers, describes the interface of one component:
.name, .description and .guard, plus the three lists .outputs, .inputs
and .locals and the convenience .is_empty, which is true when the component declares
nothing and lets the template say so rather than emit a header that is only a guard.
.guard is DDD_COMPONENT_CONTROLLER_H for a component called Controller. The word
component in the middle is deliberate: without it a component named types would
define DDD_TYPES_H before ddd_types.h was ever included, and the whole type header
would preprocess away.
Each entry of the three lists is a declaration rather than a bare object, because the same variable is declared differently in different headers:
attribute |
what it holds |
|---|---|
|
The complete declaration, semicolon included, with |
|
The preprocessor condition of this declaration, or nothing. |
|
Whether |
|
The object behind the declaration, with everything the previous table lists - which is
how a header comments a declaration with |
Helpers, and what a subdirectory is for
Four generated files that all begin with the same banner should describe that banner once.
That is what a helper template is for: a name starting with an underscore renders nothing on
its own and exists to be imported. The example set keeps its banner in _macros.jinja2,
and every other template opens with:
{% import "_macros.jinja2" as macros %}
{{- macros.banner(filename, model) }}
The macro takes what it needs as arguments, which is why the plain import is enough here.
A helper that reads model or filename directly instead of receiving them has to be
imported with context, the standard jinja2 rule: an import without it gives the imported
template a context of its own, in which those names do not exist.
A template in a subdirectory of the template directory is never rendered, but it can still be imported and included. Only the top level is scanned, so a project is free to keep fragments, per-target variants or a vendor’s original copies below the directory without each of them turning into a generated file it has to explain.
A mistake in a template stops the run
Undefined names are strict. Reading an attribute that does not exist - model.grops for
model.groups, variable.commment for variable.comment - raises instead of
rendering as an empty string, and the run ends with a traceback naming the template, the line
and the attribute:
jinja2.exceptions.UndefinedError: 'ddd.backends.c.model.CodeModel object' has no attribute 'grops'
This is worth the noise. The alternative, which is jinja2’s default, is that the typo produces nothing at all: the loop body disappears, the file is generated, the build succeeds, and a variable is missing from a header that everybody trusts. A failure at generation time is read by the person who just edited the template; a variable silently missing from an interface is found much later by somebody else.
How the files reach the disk
Every generated file is written as utf-8 with LF line endings, on every platform. Descriptions carry units and prose in any language, so utf-8 is the only sane choice, and fixed line endings mean that a file generated on Windows and the same file generated in a linux container are byte for byte identical - which matters as soon as generated code is compared across machines or checked in.
A file whose content has not changed is left untouched rather than rewritten, so its timestamp does not move and the compilation of everything that includes it is not triggered again. Regenerating a project that has not changed therefore reports:
$ ddd generate examples/demo/demo.ddd.json -o build/gen -t examples/templates
unchanged build/gen/ddd_globals.c
unchanged build/gen/ddd_globals.h
unchanged build/gen/ddd_types.h
unchanged build/gen/Controller.h
unchanged build/gen/SensorHub.h
unchanged build/gen/UserInterface.h
unchanged build/gen/EventLogger.h
unchanged build/gen/DemoDevice.a2l
Getting started
Start from the example templates rather than from an empty directory. They are shipped with the tool, and their location is printed by the tool itself rather than guessed, because a project may be built against several installations:
$ ddd templates-dir
/home/you/.venv/lib/python3.13/site-packages/ddd/templates
Copy that directory into the project, next to the description files or wherever the coding standard says generated-code templates live, and check it in - it is source code of the project now:
cp -r "$(ddd templates-dir)" tools/ddd_templates
ddd generate project.ddd.json -o build/gen -t tools/ddd_templates
Then adapt. Rename a template to rename the file it produces, edit the banner, change the
comment markers, replace model.guard(...) with whatever your standard asks for, delete a
template whose output the project does not want and add one for output it does. Nothing
refers back to the copy that was taken: ddd templates-dir is a starting point, never a
fallback. In a cmake build the directory is passed to ddd_generate as its required
TEMPLATE_DIRECTORY; see build integration.
The example templates
{% macro banner(filename, model) -%}
/*
* {{ filename }}
*
* Global variable data dictionary of project '{{ model.project }}'.
* Generated from '{{ model.source }}' by {{ model.generator }}.
*
* DO NOT EDIT - every change is lost the next time DDD runs.
*/
{%- endmacro %}
{% import "_macros.jinja2" as macros %}
{{- macros.banner(filename, model) }}
#ifndef {{ model.guard("ddd", "types") }}
#define {{ model.guard("ddd", "types") }}
{% if model.needs_stdint %}
#include <stdint.h>
{% endif %}
{% if model.needs_stdbool %}
#include <stdbool.h>
{% endif %}
{% if model.external_includes %}
/* headers defining the external types */
{% for include in model.external_includes %}
#include {{ include }}
{% endfor %}
{% endif %}
{% if model.constants %}
/* named constants of the data dictionary */
{% for constant in model.constants %}
#define {{ constant.name }} {{ constant.value }}{{ (" /**< " ~ constant.description ~ " */") if constant.description else "" }}
{% endfor %}
{% endif %}
{% for enum in model.enums %}
/* {{ enum.name }} */
typedef enum
{
{% for enumerator in enum.enumerators %}
{{ enumerator.name }} = {{ enumerator.value }}{{ "," if not loop.last else "" }}{{ (" /**< " ~ enumerator.description ~ " */") if enumerator.description else "" }}
{% endfor %}
} {{ enum.name }};
{% endfor %}
{% for structure in model.structures %}
/* {{ structure.name }}{{ " - " ~ structure.comment if structure.comment else "" }} */
typedef struct
{
{% for member in structure.members %}
{{ member.declaration }};{{ (" /**< " ~ member.comment ~ " */") if member.comment else "" }}
{% endfor %}
} {{ structure.name }};
{% endfor %}
#endif /* {{ model.guard("ddd", "types") }} */
{% import "_macros.jinja2" as macros %}
{% macro define(variable) -%}
{{ variable.qualifier }}{{ variable.c_type }} {{ variable.name }}{{ variable.array_suffix -}}
{{ ' __attribute__((section("' ~ variable.section ~ '")))' if variable.section else '' -}}
{{ ' = ' ~ variable.initializer if variable.initializer is not none else '' }};
{%- endmacro %}
{{- macros.banner(filename, model) }}
#include "ddd_globals.h"
/*
* Definition of every global variable of the project. Compile and link this
* file exactly once; DDD is the only owner of these storage locations.
*/
{% for group in model.groups %}
/* ---------------------------------------------------------------------------
* {{ group.name }}{{ " - " ~ group.description if group.description else "" }}
* ------------------------------------------------------------------------ */
{% if group.measurements and group.calibration %}
/* measurements */
{% endif %}
{% for variable in group.measurements %}
{% if variable.condition %}
#if {{ variable.condition }}
{% endif %}
{% if variable.comment %}
/** {{ variable.comment }} */
{% endif %}
{{ define(variable) }}
{% if variable.condition %}
#endif /* {{ variable.condition }} */
{% endif %}
{% endfor %}
{% if group.measurements and group.calibration %}
/* calibration data */
{% endif %}
{% for variable in group.calibration %}
{% if variable.condition %}
#if {{ variable.condition }}
{% endif %}
{% if variable.comment %}
/** {{ variable.comment }} */
{% endif %}
{{ define(variable) }}
{% if variable.condition %}
#endif /* {{ variable.condition }} */
{% endif %}
{% endfor %}
{% endfor %}
{% if not model.groups %}
/* This project does not define any global variable. */
{% endif %}
{% import "_macros.jinja2" as macros %}
{{- macros.banner(filename, model) }}
#ifndef {{ model.guard("ddd", "globals") }}
#define {{ model.guard("ddd", "globals") }}
#include "ddd_types.h"
/*
* Declarations of every global variable of the project. Software components
* shall include their own interface header instead of this file; it exists so
* that ddd_globals.c can be compiled with full prototype checking.
*/
{% for group in model.groups %}
/* ---------------------------------------------------------------------------
* {{ group.name }}{{ " - " ~ group.description if group.description else "" }}
* ------------------------------------------------------------------------ */
{% for variable in group.variables %}
{% if variable.condition %}
#if {{ variable.condition }}
{% endif %}
{% if variable.comment %}
/** {{ variable.comment }} */
{% endif %}
{{ variable.declaration() }};
{% if variable.condition %}
#endif /* {{ variable.condition }} */
{% endif %}
{% endfor %}
{% endfor %}
#endif /* {{ model.guard("ddd", "globals") }} */
{% import "_macros.jinja2" as macros %}
{{- macros.banner(filename, model) }}
/*
* Interface of software component '{{ header.name }}'.
{% if header.description %}
*
* {{ header.description }}
{% endif %}
*
* Only the variables declared in the DDD description of this component are
* visible here; everything else is intentionally out of reach.
*/
#ifndef {{ header.guard }}
#define {{ header.guard }}
#include "ddd_types.h"
{% if header.outputs %}
/* outputs - written by {{ header.name }}, read by other components */
{% for entry in header.outputs %}
{% if entry.condition %}
#if {{ entry.condition }}
{% endif %}
{% if entry.variable.comment %}
/** {{ entry.variable.comment }} */
{% endif %}
{{ entry.line }}
{% if entry.condition %}
#endif /* {{ entry.condition }} */
{% endif %}
{% endfor %}
{% endif %}
{% if header.inputs %}
/* inputs - produced elsewhere, {{ header.name }} may only read them */
{% for entry in header.inputs %}
{% if entry.condition %}
#if {{ entry.condition }}
{% endif %}
{% if entry.variable.comment %}
/** {{ entry.variable.comment }} */
{% endif %}
{{ entry.line }}{{ " /* produced by " ~ entry.variable.owner ~ " */" if entry.variable.owner else "" }}
{% if entry.condition %}
#endif /* {{ entry.condition }} */
{% endif %}
{% endfor %}
{% endif %}
{% if header.locals %}
/* locals - owned exclusively by {{ header.name }} */
{% for entry in header.locals %}
{% if entry.condition %}
#if {{ entry.condition }}
{% endif %}
{% if entry.variable.comment %}
/** {{ entry.variable.comment }} */
{% endif %}
{{ entry.line }}
{% if entry.condition %}
#endif /* {{ entry.condition }} */
{% endif %}
{% endfor %}
{% endif %}
{% if header.is_empty %}
/* This component declares no global variable. */
{% endif %}
#endif /* {{ header.guard }} */
Note
How these examples comment the generated code is not a recommendation. A different documentation convention, a MISRA deviation record, a traceability tag or no comment at all is exactly the kind of decision the templates exist to leave to the project.