-
Notifications
You must be signed in to change notification settings - Fork 1
dev_tech_docs
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.
- High-Level Architecture
- Execution Flow
- Core Domains
- Emitter Architecture
- Target-Driven Design
- Configuration → Settings Mapping
- Schema Generation & Patching
- Adding a New Target
- Adding a New Emitter
- Design Principles
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
Program.cs
Responsibilities:
- Parse CLI arguments
- Print tool version
- Route to
--init(project initialization) or--config(code generation)
No business logic lives here.
Program
├─ ConfigSchemaService
├─ ProjectInitializer (optional)
└─ CodegenRunner
├─ Load & patch config
├─ Validate schema
├─ Load GMIDL → IR
├─ Build EmitterPlan
├─ Create Emitters
└─ Execute Emitters
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.
The source of truth for extgen.
Key concepts:
- Strongly-typed configuration models
- Fully auto-generated JSON Schema
- Backwards-safe schema patching
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.
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
IosModeEmitters never re-evaluate these questions. Think of EmitterPlan as the compiler frontend and emitters as backend passes.
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.
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.
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.
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 is generated directly from ExtGenConfig:
JsonSerializerOptions.Default.GetJsonSchemaAsNode(typeof(ExtGenConfig))This guarantees the schema is always up-to-date with no hand-maintained drift.
When running with --config:
- Schema is rewritten next to the config file
-
$schemais injected or updated - Unknown JSON properties are preserved
This supports editor auto-completion, safe upgrades, and forward compatibility.
Example: adding VisionOS
- Add
VisionOsTargetConfig - Extend
ExtGenConfig.Targets - Update
EmitterPlan - Add an emitter if required
- Extend
CmakeEmitterfor presets
No existing emitter needs to change unless it supports the new target.
Example: adding Rust bindings
- Create
RustEmitterSettings - Create
RustEmitter : IIrEmitter - Map from config to settings
- Register in
CodegenRunner
Emitters do not communicate with each other.
Everything is explicit - no magic enabling, no inferred side effects.
Targets decide which languages are required, not the other way around.
If it is not in the schema, it is not supported.
All conditional logic lives in planning and validation. Emitters receive a resolved plan and emit files.
extgen is not a script. It follows a compiler-style pipeline: parse → validate → plan → emit.
GameMaker 2026