Skip to content

dev_tech_docs

DiasFranciscoA edited this page Feb 10, 2026 · 3 revisions

extgen - Architecture & Developer Documentation

This document explains the internal architecture of extgen, a schema-driven, multi-platform code generation tool for GameMaker extensions.

It is intended for:

  • Contributors extending the generator
  • Maintainers reviewing or refactoring core systems
  • 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

At a high level, 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 either:

    • --init β†’ project initialization
    • --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
  • Do not know about CMake internals

They exist to coordinate subsystems, not implement them.


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

This is 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/

Important rules:

  • Config types describe intent
  • They are not optimized for emitters
  • They mirror schema 1:1

3. Planning & Validation (EmitterPlan)

This is one of the most important architectural pieces.

EmitterPlan answers questions like:

  • Do we need C++ at all?
  • Are bindings allowed?
  • Which targets are enabled?
  • Is this configuration logically valid?

All conditional logic lives here.

NeedsCpp
AllowBindings
AllowBuild
AndroidMode
IosMode

Emitters never re-decide these things.

🧠 Think of EmitterPlan as the compiler frontend, and emitters as backend passes.


4. Emitters (extgen.Emitters)

Emitters convert IR β†’ files.

They are:

  • Stateless
  • Deterministic
  • Side-effect limited to file output

Structure:

Emitters/
 β”œβ”€ Cpp/
 β”œβ”€ Gml/
 β”œβ”€ Android/
 β”‚   β”œβ”€ Java
 β”‚   β”œβ”€ Kotlin
 β”‚   └─ Jni
 β”œβ”€ AppleMobile/
 β”‚   β”œβ”€ Objc
 β”‚   β”œβ”€ Swift
 β”‚   └─ ObjcNative
 β”œβ”€ Cmake/
 └─ Doc/

Each emitter:

  • Accepts an EmitterSettings object
  • Emits files based on IR
  • Does not read config directly

Emitter Architecture

Each emitter follows the same contract:

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

Why this matters

  • Emitters can be unit tested in isolation
  • Emitters can be reordered safely
  • Emitters do not depend on CLI or config formats

Target-Driven Design

extgen is not language-driven.

❌ Bad mental model:

β€œEnable C++, enable GML, enable iOS”

βœ… Correct model:

β€œI am targeting iOS in native mode, therefore I need C++, ObjC glue, and CMake support.”

Targets decide:

  • Which emitters run
  • Which languages are required
  • Which build artifacts exist

This avoids invalid states like:

  • β€œSwift enabled without iOS”
  • β€œJNI enabled without Android”

Configuration β†’ Settings Mapping

You correctly identified that enforcing a generic interface like:

IFromConfig<TSettings, TConfig>

was conceptually wrong.

Final Design (Correct)

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

Example:

AndroidEmitterSettings.ToSettings(AndroidTargetConfig cfg)

Why this is correct:

  • No magic interfaces
  • No reflection
  • No enforced coupling
  • Easy to debug
  • Easy to change

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:

  • Schema is always up-to-date
  • No hand-maintained schema drift

Schema Patching

When running with --config:

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

This enables:

  • Editor auto-completion
  • Safe upgrades
  • Forward compatibility

πŸš€ Adding a New Target

Example: Adding VisionOS

Steps:

  1. Add VisionOsTargetConfig
  2. Extend ExtGenConfig.Targets
  3. Update EmitterPlan
  4. Add emitter (if needed)
  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 β†’ settings
  4. Register in CodegenRunner

Emitters never talk to each other.


Design Principles

1. No Implicit Behavior

Everything is explicit:

  • No β€œmagic” enabling
  • No inferred side effects

2. Target > Language

Targets decide languages, not the other way around.


3. Schema Is the Contract

If it’s not in the schema, it’s not supported.


4. Emitters Are Dumb (On Purpose)

All intelligence lives in:

  • Planning
  • Validation
  • Configuration

5. Production-Grade First

This is not a script. It is a compiler-style toolchain.