-
Notifications
You must be signed in to change notification settings - Fork 0
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
- 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