-
Notifications
You must be signed in to change notification settings - Fork 1
getting_started_sample
This page walks through creating a minimal extgen project from scratch. By the end, you will have a working Windows extension with a function that takes a callback, a typed struct, and a typed enum - covering the most common GMIDL patterns in a small, self-contained example.
A Counter extension that:
- Keeps a running total.
- Returns results as a typed struct (
CounterResult). - Reports its state via a typed enum (
CounterStatus). - Accepts a GML callback on increment.
- extgen installed and on
PATH - CMake 3.25+
- Visual Studio 2022
Run extgen with --init to create the project folder. The argument is the full path to where the CMake project will live - extgen writes config.json and extgen.schema.json directly into that folder.
extgen --init path/to/counterThis generates:
counter/
config.json
extgen.schema.json
config.json is the starting point for configuration. extgen.schema.json enables editor auto-complete and is rewritten automatically on each run.
{
"$schema": "./extgen.schema.json",
"name": "Counter",
"gmidl": "spec.gmidl",
"targets": {
"windows": {
"enabled": true
}
},
"gamemaker": {
"wrappers": {
"output": "../scripts/Counter_api/Counter_api.gml"
},
"extension": {
"mode": "patch",
"path": "../Counter.yy"
}
}
}The wrappers.output path is relative to config.json. Adjust it to match your GameMaker project layout.
extension.mode = patch means extgen edits an existing GameMaker extension asset rather than creating a new one. Set extension.path to the .yy file of the extension you have already created in GameMaker.
Create spec.gmidl in the same folder as config.json.
[gml_api]
module Counter;
[global, bind = typed_gml]
enum CounterStatus
{
Ok = 0,
Clamped = 1,
Error = 2,
}
[global]
class `[[CounterResult]]` prototype Object
{
[field]
property total : int32 { get; set; }
[field, type_hint = `CounterStatus`]
property status : gmval { get; set; }
}
[global, bind = typed_gml]
function counter_reset() : unit;
[global, bind = typed_gml, type_hint = `CounterResult`]
function counter_add(amount : int32, callback : func) : gmval;
[global, bind = typed_gml]
function counter_get() : int32;
| Symbol | Kind | Notes |
|---|---|---|
CounterStatus |
Enum | Strongly typed in GML via bind = typed_gml
|
CounterResult |
Struct/class | Returned to GML as a typed struct |
counter_reset |
Function | Resets the counter; returns nothing |
counter_add |
Function | Adds to the counter; fires a callback; returns CounterResult
|
counter_get |
Function | Returns the current total as int32
|
counter_add uses two advanced GMIDL features:
-
callback : func- a GML function reference, passed asGMFunctionin C++ -
type_hint = `CounterResult`on the function attribute - tells extgen the return value is aCounterResultstruct
Run extgen once to generate the rest:
extgen --config 'path/to/the/config.json'After generation, the folder looks like:
project/
config.json
spec.gmidl
CMakeLists.txt
CMakePresets.json
code_gen/
core/
GMExtUtils.h
GMExtWire.h
native/
CounterInternal_native.h
CounterInternal_native.cpp
CounterInternal_exports.h
src/
CMakeLists.txt
native/
Counter_native.cpp <- you write this
Note
Everytime you make changes to the config.json file of to the spec.gmidl file you must to re-run the tool.
These files are shared across all native targets. Do not hand-edit them.
-
GMExtWire.h- marshaling helpers, type conversions,GMWIRE_THROW,GMFunction,GMBuffer -
GMExtUtils.h- utility macros
Generated for the native target (Windows, macOS, Linux, consoles).
-
CounterInternal_native.h- declares the C++ structs and free functions your implementation must provide:struct CounterResult { int32_t total{}; CounterStatus status{}; }; enum class CounterStatus : int32_t { Ok = 0, Clamped = 1, Error = 2, }; void counter_reset(); CounterResult counter_add(int32_t amount, gm::wire::GMFunction callback); int32_t counter_get();
-
CounterInternal_native.cpp- generated bridge layer that unpacks GML values, calls your functions, and packs the results back. You do not edit this. -
CounterInternal_exports.h- the DLL export table. You do not edit this.
extgen also writes a .gml file to the path you specified in wrappers.output. This file contains GML wrapper functions and the typed constructor for CounterResult.
This is the only file you write. It implements the three functions declared in CounterInternal_native.h.
#include "native/CounterInternal_native.h"
static int32_t g_total = 0;
static const int32_t k_max = 1000;
void counter_reset()
{
g_total = 0;
}
CounterResult counter_add(int32_t amount, gm::wire::GMFunction callback)
{
CounterResult result;
g_total += amount;
if (g_total > k_max)
{
g_total = k_max;
result.status = CounterStatus::Clamped;
}
else
{
result.status = CounterStatus::Ok;
}
result.total = g_total;
// Fire the GML callback with the current total.
// call() is thread-safe - safe to invoke from a worker thread.
callback.call(result.total);
return result;
}
int32_t counter_get()
{
return g_total;
}-
GMFunction::call()accepts numeric scalars,std::string,std::string_view,const char*, and custom structs/enums. - The callback fires asynchronously here. The call will queue the execution and GameMaker runtime will execute it on the next frame.
-
g_totalis a process-global here for simplicity. For a real extension with multiple instances, use a handle-based design - store state in a map keyed on an ID, and put that ID in a struct field.
extgen generates a starter src/CMakeLists.txt. For a Windows-only extension it looks like this:
set(SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
if(WIN32)
file(GLOB TARGET_SRCS CONFIGURE_DEPENDS "${SRC_DIR}/native/*.cpp")
endif()
target_sources(${PROJECT_NAME} PRIVATE ${TARGET_SRCS})No changes are needed for this sample. Add more file(GLOB ...) branches when you add more platform targets.
-
Edit the configuration

-
Selecte
Windows x64(you can use either Debug or Release)
-
Press the build button (note your configs will be different than then ones present in the image below, it's okay)

cmake --preset windows-debugOr for release:
cmake --preset windows-releasecmake --build --preset windows-debugThe post-build step copies the DLL to the extension output folder next to your .yyp.
| File | Description |
|---|---|
Counter.dll |
Windows shared library, copied to extension folder |
After building, open GameMaker and run the project. The generated wrappers provide:
counter_reset();
var result = counter_add(10, function(total) {
show_debug_message("Callback fired, total = " + string(total));
});
// result is a CounterResult struct - returned synchronously
show_debug_message(result.total); // 10
show_debug_message(result.status); // CounterStatus.Ok
var current = counter_get(); // 10result is returned synchronously. The callback always fires on the next GameMaker frame - even when callback.call() is invoked immediately from the C++ side.
CounterStatus is a strongly-typed GML enum because the GMIDL declares it with bind = typed_gml. Comparisons like result.status == CounterStatus.Ok work with full editor auto-complete.
- Add more platforms by extending
config.jsonandsrc/CMakeLists.txt. - For consoles (Switch, PS4, PS5), see Console Setup.
- For iOS/tvOS targets, see Implementation Workflow - iOS/tvOS.
- Read the full GMIDL Introduction for optional types, collections, fixed-size arrays, and buffer parameters.
GameMaker 2026