Skip to content

getting_started_sample

DiasFranciscoA edited this page Jun 25, 2026 · 11 revisions

Getting Started: A Sample Extension

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.


What you will build

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.

Prerequisites

  • extgen installed and on PATH
  • CMake 3.25+
  • Visual Studio 2022

Project Layout

Open GameMaker IDE and start by creating and extension Counter (this will create the following path <project_root>/extensions/Counter/Counter.yy). Now make sure we add a new file to the extension we will use a generic file (.ext) and call it Counter.ext. Now inside the extension lets create a source folder (this is the standard we use for our official extensions).

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 "<project_root>/extensions/Counter/source"

This generates:

source/
  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.


config.json

{
  "$schema": "./extgen.schema.json",
  "root": "./",
  "input": "spec.gmidl",
  "targets": {
    "windows": {
      "enabled": true
    }
  },
  "gamemaker": {
    "wrappers": {
      "outputFile": "../../../scripts/Counter_api/Counter_api.gml"
      "extensionName": "Counter",
      "extensionFileName": "Counter.ext"
    },
    "extension": {
      "mode": "patch",
      "path": "../../../extensions/Counter.yy"
    },
    "runtime": {
      "outputFile": "../../../scripts/ExtensionCore_runtime/ExtensionCore_runtime.gml"
    },
  }
}

The wrappers.outputFile path is relative to config.json. Adjust it to match your GameMaker project layout.

Important

This GML file MUST exist as extgen will not add new files to your project.

The wrappers.extensionName is the name of the extension. Adjust it to match your extension name (Counter). The wrappers.extensionFileName is the name of the generic file inside the extension. Adjust it to match the extension file name you are using (Counter.ext).

Note

The entries wrappers.extensionName and wrappers.extensionFileName are optional if the extension name and filename uses the same name as the GMIDL module name.

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.

Important

This extension file MUST exist extgen will not add new files to your project.

Tip

You also have a gamemaker.runtime that you can configure:

"runtime" : { "outputFile": "../../../scripts/Core_runtime/Core_runtime.gml" }

This tells extgen to also write the runtime functions it needs to work. This runtime is shared across all extensions so if you already have it from another extension you can ignore this entry. Note as with previous project files this file needs to be created manually from the IDE.

Note

targets.<targetName> is enable by default if the entry exists. So if you want to disable it you need to manually set targets.windows.enabled = false this will disable it. The same applies to other sections that also have "enabled" properties.

Important

For GMRT compatibility you will need to also turn on the gamemaker.injectors = {} nothing else needs to be added here the entry just needs to exist. This will tell extgen to generate some code injectors that are picked up by the GMRT compiler during project build. You can specify an "outputFolder" property that is relative to the config.json file. The default is "../injections" which falls correctly into your <extension_folder>/injections where it should be. Adjust accordingly.


spec.gmidl

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;

What this defines

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 as GMFunction in C++
  • type_hint = `CounterResult` on the function attribute - tells extgen the return value is a CounterResult struct

Generating the code

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.


What extgen generates

code_gen/core/

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

code_gen/native/

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.

GML output

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.


src/native/Counter_native.cpp

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;
}

Notes

  • 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_total is 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.

src/CMakeLists.txt

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.


Build (Windows)

Visual Studio Code

  1. Edit the configuration

  2. Selecte Windows x64 (you can use either Debug or Release)

  3. Press the build button (note your configs will be different than then ones present in the image below, it's okay)

Command Line Interface

1. Configure

cmake --preset windows-debug

Or for release:

cmake --preset windows-release

2. Build

cmake --build --preset windows-debug

3. Output

The 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

Using the extension in GML

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();          // 10

result 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.


Next steps

Clone this wiki locally