-
Notifications
You must be signed in to change notification settings - Fork 2
dev_tech_docs
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
- High-Level Architecture
- Execution Flow
-
Core Domains
- App Layer
- Configuration & Schema
- Planning & Validation
- Emitters
- Build System (CMake)
- Emitter Architecture
- Target-Driven Design
- Configuration → Settings Mapping
- Schema Generation & Patching
- Adding a New Target
- Adding a New Emitter
- Design Principles
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
Program.cs
Responsibilities:
-
Parse CLI arguments
-
Print tool version
-
Route to either:
-
--init→ project initialization -
--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:
CodegenRunnerProjectInitializerConfigSchemaService
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.
This is 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/
Important rules:
- Config types describe intent
- They are not optimized for emitters
- They mirror schema 1:1
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
IosModeEmitters never re-decide these things.
🧠 Think of
EmitterPlanas the compiler frontend, and emitters as backend passes.
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
Each emitter follows the same contract:
interface IIrEmitter
{
void Emit(IrCompilation ir, string outputDir);
}- Emitters can be unit tested in isolation
- Emitters can be reordered safely
- Emitters do not depend on CLI or config formats
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”
You correctly identified that enforcing a generic interface like:
IFromConfig<TSettings, TConfig>was conceptually wrong.
-
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 is generated directly from ExtGenConfig:
JsonSerializerOptions.Default.GetJsonSchemaAsNode(typeof(ExtGenConfig))This guarantees:
- Schema is always up-to-date
- No hand-maintained schema drift
When running with --config:
- Schema is rewritten next to config
-
$schemais injected or updated - Unknown JSON properties are preserved
This enables:
- Editor auto-completion
- Safe upgrades
- Forward compatibility
Example: Adding VisionOS
Steps:
- Add
VisionOsTargetConfig - Extend
ExtGenConfig.Targets - Update
EmitterPlan - Add emitter (if needed)
- Extend CMakeEmitter for 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 → settings
- Register in
CodegenRunner
Emitters never talk to each other.
Everything is explicit:
- No “magic” enabling
- No inferred side effects
Targets decide languages, not the other way around.
If it’s not in the schema, it’s not supported.
All intelligence lives in:
- Planning
- Validation
- Configuration
This is not a script. It is a compiler-style toolchain.
GameMaker 2026