When engineering complex, heterogeneous systems—such as real-time C++ simulation kernels communicating with C# operator displays, Rust network bridges, and Python telemetry analysis pipelines—maintaining synchronized data models and serialization code by hand is tedious and error-prone. A single mismatched byte offset, unaligned integer, or forgotten struct member can cause silent memory corruption or subtle network protocol failures across platforms.

ENTDLR (Entity Definition Language Redux) is an extensible Interface Definition Language (IDL) compiler and multi-language code generation engine built in modern C++17. It takes schema definitions written in an extended Google FlatBuffers grammar, parses them through an ANTLR4 frontend into a strongly typed in-memory Abstract Syntax Tree (Context), and renders production-ready code through the Inja template engine—augmented by an embedded Wren scripting runtime for complex generation logic.


High-Level System Architecture

Rather than hardcoding language-specific generators directly into the compiler executable (like protoc or flatc), ENTDLR strictly decouples grammar parsing from target emission. The compiler acts as a pure frontend that constructs a serializable AST model, while target languages, serialization formats, and documentation layouts are defined entirely through external template and script files.

flowchart TD
    subgraph ParserSubsystem ["Parser Subsystem (entdlr::parser)"]
        direction TB
        FBS[".fbs Schema Files"] --> G4["ANTLR4 Frontend
• FlatBuffersLexer / Parser
• JavadocLexer / Parser"] G4 --> INC["Include Graph Resolver
Resolves relative imports & detects circular cycles"] INC --> BUILD["Context Builder & Sorter
Constructs AST & standardizes token ordering"] end subgraph ContextSubsystem ["In-Memory AST Context (entdlr::context)"] direction TB BUILD --> CTX["Entdlr::Context Model
• Includes & Namespaces
• Enums, Unions & Structs
• Fields & Fixed/Dynamic Arrays
• Methods (in/out, static, mutable)
• Interfaces & Services
• Attributes & Javadoc Tag Maps"] CTX --> JSON_SERIALIZE["json_helpers::dumpContext()
Serializes AST to nlohmann::json model beneath fbs"] end subgraph TemplateSubsystem ["Template & Scripting Engine (entdlr::template)"] direction TB CFG["config.json
type_map (type overrides)
json (user config root)"] --> TMAP["TypeMap Engine
Recursively substitutes mapped types across AST"] JSON_SERIALIZE --> TMAP TMAP --> INJA["InjaTemplate Execution
Renders .tmpl control flow, loops & expressions"] subgraph WrenBridge ["Embedded Wren VM Bridge"] INJA -->|"Unknown function invocation"| DISPATCH["Dynamic Function Dispatcher
Resolves method on class Functions"] DISPATCH --> MARSHAL_IN["Slot Marshaler (JSON → Wren)
Recursively maps JSON to Wren Map, List, Num, String"] MARSHAL_IN --> WREN_EXEC["Embedded Wren VM
Executes static methods & transforms"] WREN_EXEC --> MARSHAL_OUT["Slot Marshaler (Wren → JSON)
Converts Wren return slots to nlohmann::json"] MARSHAL_OUT -->|"Structured result (Map/List/Value)"| INJA end end subgraph Interfaces ["Invocation Frontends"] CLI["CLI Executable
entdlr [args]"] CMAKE["CMake Build Helper
entdlr_generate()"] PY["Python Extension
nanobind bindings"] CPP_API["C++ Public API
parser.h & inja_template.h"] end INJA --> OUT["Target Output Stream
File or string buffer"] CLI -.-> ParserSubsystem CMAKE -.-> ParserSubsystem PY -.-> ParserSubsystem CPP_API -.-> ParserSubsystem
  1. Parser & Semantic Validation: Reads .fbs definition files, resolves recursive includes with circular dependency detection, and builds an in-memory Context containing all namespaces, types, fields, methods, docstrings, and custom attributes.
  2. Context Transformation & Configuration: Applies an optional JSON configuration file providing type-mapping rules (e.g., mapping primitive IDL types uint32 to std::uint32_t) and custom build parameters.
  3. Template & Script Engine: Evaluates .tmpl Inja templates against the AST context (fbs). Whenever a template requires complex procedural logic—such as identifier casing conversions, bitfield packing calculations, or multi-field validation—it transparently delegates to embedded Wren functions.

Extended IDL Grammar

ENTDLR adopts the familiar, human-readable schema syntax of Google FlatBuffers, but extends it significantly with first-class primitives for object-oriented contracts, service architectures, and interface boundaries:

namespace Telemetry;

/// Coordinate reference frames supported by tracking sensors
enum CoordinateFrame : uint8 {
    ECEF = 0,
    NED = 1,
    Body = 2
}

/**
 * 3D Spatial Vector with explicit units and metadata attributes
 * @units meters
 */
struct Vector3 (display: "vector", align: 8) {
    x: float64 (min: -1e7, max: 1e7);
    y: float64 (min: -1e7, max: 1e7);
    z: float64 (min: -1e7, max: 1e7);

    // Static factory methods and instance operations
    static origin(): Vector3;
    dot(other: in Vector3): float64;
    mutable normalize();
}

/**
 * Pure virtual interface defining a telemetry consumer
 */
interface ITelemetrySink {
    recordPosition(entityId: uint32, pos: in Vector3): bool;
    flush();
}

/**
 * Service component defining input and registration endpoints
 */
service RadarStream (transport: "zeromq", port: 5555) {
    TrackUpdate: input;
    Subscription: register;
}

Grammar Enhancements over Standard FlatBuffers

  • Method Signatures on Structs & Tables: Supports static methods (static origin(): Vector3;), constant member functions, and mutable member functions (mutable normalize();).
  • Directional Parameter Qualifiers: Method arguments can explicitly specify in, out, or both to model reference passing and output parameters across language boundaries.
  • Pure Interfaces: The interface keyword defines a set of callable contracts containing methods without data fields.
  • Service Declarations: The service keyword groups typed components under application-defined communication roles (e.g. input, output, register).
  • Fixed-Size & Dynamic Arrays: Supports unsized dynamic vectors ([uint32]) as well as memory-aligned, fixed-capacity arrays ([float32:3]).
  • Javadoc & Line Documentation: Triple-slash (///) comments and Javadoc block comments (/** ... @tag value */) are parsed into structured docstrings with associated tags and line locations.

Inja Template Architecture

ENTDLR embeds the Inja template engine (a modern C++ template library inspired by Jinja and Django). The template execution environment exposes the parsed AST under the root object fbs, and any optional JSON configuration parameters under json.

Example: C++ Header Generation Template (cpp_header.tmpl)

Here is an example of an ENTDLR template that iterates over the AST to generate type-safe modern C++ struct definitions:

#pragma once
#include <cstdint>
#include <string>
#include <vector>

## for namespace in fbs.namespaces
namespace {{ namespace.name }} {

## for enum in namespace.enums
{{ indent("// " + enum.comment, 0) }}
enum class {{ enum.name }} : {{ enum.type }} {
## for value in enum.values
    {{ value.name }} = {{ value.value }}{% if loop.is_last == false %},{% endif %}
## endfor
};

## endfor

## for struct in namespace.structs
{{ indent("/**\n * " + struct.comment + "\n */", 0) }}
struct {{ struct.name }} {
## for field in struct.fields
    {{ field.type }} {{ field.name }}{% if field.isArray %}[{{ field.arraySize }}]{% endif %};
## endfor

## for method in struct.methods
    {% if method.isStatic %}static {% endif %}{{ method.returnType }} {{ method.name }}(
## for param in method.parameters
        {% if param.constant %}const {% endif %}{{ param.type }}& {{ param.name }}{% if loop.is_last == false %}, {% endif %}
## endfor
    );
## endfor
};

## endfor
} // namespace {{ namespace.name }}
## endfor

Template authors have access to loops (## for), conditionals ({% if %}), Inja built-in functions (length(), upper(), lower()), and ENTDLR-specific helpers such as getTokenType(), dump_context(), float_to_int(), and env_default().


Embedded Wren Scripting

While template engines excel at basic string substitution and looping, real-world code generation inevitably runs into problems that are painful or impossible to express cleanly in template markup alone:

  • Translating identifiers between conventions (snake_case, camelCase, PascalCase, SCREAMING_SNAKE_CASE).
  • Computing memory layouts, struct padding, and bitfield offsets.
  • Generating language-specific type initializers and default values.
  • Filtering fields based on custom attribute logic or metadata tags.

Rather than cluttering templates with convoluted conditionals or forcing developers to fork the C++ compiler to add custom filters, ENTDLR embeds Wren—a fast, class-based, concurrent scripting language designed specifically for embedding in C and C++ applications.

How the Inja-Wren Bridge Works

When an Inja template encounters an unknown function call, ENTDLR automatically routes the call into the embedded Wren virtual machine:

  1. The Functions Class: A user-supplied Wren script declares a class named Functions. Every static method on this class is dynamically exposed as a template function.
  2. Lossless Argument Marshaling: Inja expressions—including nested objects, lists, strings, numbers, booleans, and null—are converted directly into native Wren Map, List, String, and Num objects. An entire AST subtree (like an entire struct or field object) can be passed straight into Wren.
  3. Structured Returns: Wren functions are not limited to returning plain strings; they can return lists and string-keyed maps that Inja can immediately iterate over or unpack using {% set %} statements.

Example: functions.wren

class Functions {
    // Converts snake_case or lowercase identifiers to PascalCase
    static toPascalCase(ident) {
        var parts = ident.split("_")
        var result = ""
        for (part in parts) {
            if (part.count > 0) {
                result = result + part[0].codePoints[0].toUpper + part[1..-1]
            }
        }
        return result
    }

    // Resolves a language-specific default initializer based on type and attributes
    static cppDefault(field) {
        var type = field["type"]
        if (type == "string") return "\"\""
        if (type == "bool") return "false"
        if (field["isArray"]) return "{}"
        if (field["attributes"].containsKey("default")) {
            return field["attributes"]["default"]["value"].toString
        }
        return "0"
    }

    // Inspects custom attributes and generates documentation metadata
    static summarizeField(field) {
        var attrs = field["attributes"]
        var units = attrs.containsKey("units") ? attrs["units"]["value"] : "none"
        var isSigned = field["type"].startsWith("int")

        return {
            "name": field["name"],
            "typeName": field["type"],
            "units": units,
            "isSigned": isSigned,
            "cppInit": cppDefault(field)
        }
    }
}

Calling Wren from the Template

## for field in struct.fields
{% set meta = summarizeField(field) %}
// Field: {{ toPascalCase(field.name) }} | Units: {{ meta.units }}
{{ meta.typeName }} m_{{ field.name }} = {{ meta.cppInit }};
## endfor

Because the bridge passes copies of the AST context into Wren’s VM slots, scripts can freely inspect, filter, and transform metadata without side effects on the rest of the compilation pipeline.


Configuration & Type Mapping

In multi-target pipelines, primitive types frequently need to be adjusted without modifying the underlying .fbs schema files. ENTDLR allows developers to supply an external JSON configuration file containing a type_map and arbitrary json variables:

{
  "type_map": {
    "uint32": "std::uint32_t",
    "uint64": "std::uint64_t",
    "float32": "float",
    "float64": "double",
    "string": "std::string"
  },
  "json": {
    "include_guard": "SIMULATION_TELEMETRY_MESSAGES_HPP",
    "export_macro": "SIM_API",
    "generate_json_codecs": true
  }
}
  • type_map: Automatically substitutes type identifiers in a copy of the context across enum underlying types, struct fields, method return types, and parameters before rendering.
  • json: Injects arbitrary user configuration values directly into the template root, accessible as {{ json.include_guard }}, {{ json.export_macro }}, etc.

Multi-Target Integration Ecosystem

ENTDLR is designed to integrate into modern software engineering toolchains at whatever level is most convenient:

1. Command-Line Interface (CLI)

The standalone entdlr binary can be run manually or scripted in shell pipelines:

# Generate a C++ header from schema using Inja template, Wren script, and config
entdlr cpp.tmpl message.fbs \
    --config cpp.json \
    --wren functions.wren \
    --output generated/message.hpp

# Recursively parse an entire directory of schemas
entdlr doc.tmpl --dir schemas/ --output docs/api.md

2. CMake Build Integration (entdlr_generate)

ENTDLR provides a first-class CMake integration module (entdlr_generate()) that automates code generation as part of standard build workflows:

find_package(entdlr 0.10 CONFIG REQUIRED)

add_library(telemetry_lib messages.cpp)

entdlr_generate(
    TARGET telemetry_lib
    DEFINITION "${CMAKE_CURRENT_SOURCE_DIR}/telemetry.fbs"
    TEMPLATE "${CMAKE_CURRENT_SOURCE_DIR}/templates/cpp.tmpl"
    WREN "${CMAKE_CURRENT_SOURCE_DIR}/templates/helpers.wren"
    CONFIG "${CMAKE_CURRENT_SOURCE_DIR}/templates/config.json"
    OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/generated/telemetry.hpp"
)

target_include_directories(telemetry_lib PUBLIC "${CMAKE_CURRENT_BINARY_DIR}/generated")

The CMake macro automatically registers custom build rules and dependencies, ensuring that generated code is re-compiled whenever the schema, template, or Wren helper script changes.

3. High-Performance Python API (nanobind)

ENTDLR includes a complete, typed Python extension package built with nanobind:

from pathlib import Path
import entdlr

# Parse schemas directly into strongly typed AST objects
context = entdlr.parse_file(Path("telemetry.fbs"))

for namespace in context.namespaces:
    print(f"Namespace: {namespace.name}")
    for struct in namespace.structs:
        print(f"  Struct {struct.name} ({len(struct.fields)} fields):")
        for field in struct.fields:
            print(f"    - {field.name}: {field.type}")

# Render templates with configuration and optional Wren script
output = entdlr.render(
    context,
    template_string="{{ json.banner }}: {{ fbs.namespaces.0.name }}",
    wren_script="class Functions { static banner(s) { return s.toUpper } }",
    config={"json": {"banner": "GENERATED CODE"}},
)

# Lossless dictionary round-tripping for JSON pipelines
ast_dict = context.to_dict()
reconstituted_context = entdlr.Context.from_dict(ast_dict)

4. C++ Parser & Template Libraries

For applications that require dynamic schema processing or on-the-fly code generation inside a running engine:

#include <entdlr/parser.h>
#include <entdlr/inja_template.h>

// Parse schema file into AST Context
auto context = Entdlr::Parser::parseFile("telemetry.fbs");
context.sort();

// Execute template rendering with Wren script and config JSON
Entdlr::InjaTemplate engine;
std::string result = engine.applyString(context, templateContent, wrenScript, configJson);

Summary

By unifying FlatBuffers schema ergonomics, ANTLR4 grammar validation, Inja template flexibility, and embedded Wren algorithmic scripting, ENTDLR solves the schema-to-code problem with zero hardcoded language biases. Whether compiling thousands of simulation packets into bit-exact C++ memory structures, producing C# game-engine wrappers, or validating data pipelines in Python, it provides a clean, maintainable single source of truth across the entire tech stack.