Skip to content

dev_tech_docs

Francisco Dias edited this page Apr 9, 2026 · 5 revisions

extgen - Architecture & Developer Documentation

This document describes the internal architecture of extgen. It is intended for contributors extending the generator, maintainers reviewing or refactoring core systems, and advanced users embedding extgen in larger build pipelines.


Table of Contents

  1. High-Level Architecture
  2. Execution Flow
  3. Core Domains
  4. Emitter Architecture
  5. Target-Driven Design
  6. Configuration → Settings Mapping
  7. Schema Generation & Patching
  8. Adding a New Target
  9. Adding a New Emitter
  10. Design Principles

High-Level Architecture

extgen follows a strict pipeline:

Config (JSON + Schema)
        ↓
Validation & Planning
        ↓
Emitter Selection
        ↓
IR → Code Emission
        ↓
Build System Emission (CMake)

Key characteristics:

  • Schema-first - all configuration is validated and documented via JSON Schema
  • Target-driven - platforms determine what code is generated
  • Stateless emitters - emitters are pure functions over IR + settings
  • Explicit planning - all decisions are resolved before emission begins

Execution Flow

Entry Point

Program.cs

Responsibilities:

  • Parse CLI arguments
  • Print tool version
  • Route to --init (project initialization) or --config (code generation)

No business logic lives here.


Runtime Flow

Program
 ├─ ConfigSchemaService
 ├─ ProjectInitializer (optional)
 └─ CodegenRunner
      ├─ Load & patch config
      ├─ Validate schema
      ├─ Load GMIDL → IR
      ├─ Build EmitterPlan
      ├─ Create Emitters
      └─ Execute Emitters

Core Domains

1. App Layer (extgen.App)

Purpose: Orchestration only.

Key classes: CodegenRunner, ProjectInitializer, ConfigSchemaService.

These classes do not generate code, do not contain platform logic, and do not know about CMake internals. They coordinate subsystems.


2. Configuration & Schema (extgen.Models.Config)

The source of truth for extgen.

Key concepts:

  • Strongly-typed configuration models
  • Fully auto-generated JSON Schema
  • Backwards-safe schema patching

Structure

Models/Config
 ├─ ExtGenConfig
 ├─ Targets/
 │   ├─ WindowsTargetConfig
 │   ├─ AndroidTargetConfig
 │   ├─ IosTargetConfig
 │   └─ ...
 ├─ Build/
 └─ Extras/

Config types describe intent. They are not optimized for emitters and mirror the schema 1:1.


3. Planning & Validation (EmitterPlan)

EmitterPlan resolves all conditional logic before emission begins:

  • Does this configuration need C++ at all?
  • Are bindings permitted?
  • Which targets are enabled?
  • Is this configuration logically valid?
NeedsCpp
AllowBindings
AllowBuild
AndroidMode
IosMode

Emitters never re-evaluate these questions. Think of EmitterPlan as the compiler frontend and emitters as backend passes.


4. Emitters (extgen.Emitters)

Emitters convert IR to files. They are stateless, deterministic, and limited to file output as a side effect.

Emitters/
 ├─ Cpp/
 ├─ Gml/
 ├─ Android/
 │   ├─ Java
 │   ├─ Kotlin
 │   └─ Jni
 ├─ AppleMobile/
 │   ├─ Objc
 │   ├─ Swift
 │   └─ ObjcNative
 ├─ Cmake/
 └─ Doc/

Each emitter accepts an EmitterSettings object and emits files based on IR. Emitters do not read config directly.


Emitter Architecture

Each emitter follows the same contract:

interface IIrEmitter
{
    void Emit(IrCompilation ir, string outputDir);
}

This design allows emitters to be unit tested in isolation, safely reordered, and developed without coupling to CLI or config formats.


Target-Driven Design

extgen is not language-driven.

The incorrect mental model: "Enable C++, enable GML, enable iOS."

The correct model: "I am targeting iOS in native mode, therefore C++, ObjC glue, and CMake support are required."

Targets decide which emitters run, which languages are required, and which build artifacts exist. This avoids invalid states such as Swift enabled without iOS, or JNI enabled without Android.


Configuration → Settings Mapping

Config models live in Models.Config. EmitterSettings live near their emitters. Mapping happens in explicit mappers:

AndroidEmitterSettings.ToSettings(AndroidTargetConfig cfg)

No generic interfaces, no reflection, no enforced coupling. Mapping is orchestration logic, not domain logic.


Schema Generation & Patching

Schema Generation

Schema is generated directly from ExtGenConfig:

JsonSerializerOptions.Default.GetJsonSchemaAsNode(typeof(ExtGenConfig))

This guarantees the schema is always up-to-date with no hand-maintained drift.


Schema Patching

When running with --config:

  • Schema is rewritten next to the config file
  • $schema is injected or updated
  • Unknown JSON properties are preserved

This supports editor auto-completion, safe upgrades, and forward compatibility.


Adding a New Target

Example: adding VisionOS

  1. Add VisionOsTargetConfig
  2. Extend ExtGenConfig.Targets
  3. Update EmitterPlan
  4. Add an emitter if required
  5. Extend CmakeEmitter for presets

No existing emitter needs to change unless it supports the new target.


Adding a New Emitter

Example: adding Rust bindings

  1. Create RustEmitterSettings
  2. Create RustEmitter : IIrEmitter
  3. Map from config to settings
  4. Register in CodegenRunner

Emitters do not communicate with each other.


Design Principles

No implicit behavior

Everything is explicit - no magic enabling, no inferred side effects.

Target over language

Targets decide which languages are required, not the other way around.

Schema is the contract

If it is not in the schema, it is not supported.

Emitters are intentionally simple

All conditional logic lives in planning and validation. Emitters receive a resolved plan and emit files.

Compiler-style toolchain

extgen is not a script. It follows a compiler-style pipeline: parse → validate → plan → emit.