Skip to content

user_gmidl_docs

DiasFranciscoA edited this page Feb 24, 2026 · 7 revisions

GMIDL Introduction 🧩

How to Define Your API for extgen

GMIDL (GameMaker Interface Definition Language) is how you describe your extension API.

From one .gmidl file, extgen generates:

  • 🎮 GML bindings
  • 🧠 C++ bridge code
  • 📱 Android glue
  • 🍎 iOS / tvOS glue
  • 🎮 Console integration
  • 🛠 CMake projects

This guide explains GMIDL in the simplest possible way.


📚 Table of Contents

  1. Basic Structure
  2. Attributes
  3. Type Rules
  4. Using type_hint
  5. Collections
  6. Optional Types
  7. Functions
  8. Classes (Structs)
  9. Enums
  10. Quick Rules Summary
  11. Special Types: Functions & Buffers

🧱 1. Basic Structure

A GMIDL file looks like this:

[gml_api]
module MyExtension;

// Functions
[global, bind = typed_gml]
function my_function() : unit;

// Classes
[global]
class `[[MyStruct]]` prototype Object
{
    [field]
    property value : string { get; set; }
}

// Enums
[global, bind = typed_gml]
enum MyEnum {
    A,
    B
}

You define:

  • Functions
  • Classes (structs)
  • Enums

🏷 2. Attributes (Very Important)

Anything inside brackets [...] is called an attribute.

Attributes modify how extgen generates bindings, wrappers, metadata, and editor behavior.

Example:

[global, bind = typed_gml]
function my_function() : unit;

🔎 What Are Attributes?

Attributes can be:

  • Flagsglobal
  • 🔧 Key/value pairsbind = typed_gml
  • 🧠 Type modifierstype_hint = \uint64``

They can be applied to:

  • Functions
  • Classes
  • Properties ([field])
  • Class functions
  • Structs

📌 Commonly Used Attributes

global

Marks a function or class as globally accessible in GML.

[global]
function do_something() : unit;

Important

GameMaker functions are globally accessible by default.

Currently, extgen does not enforce namespace scoping differences for global.

However, this attribute exists for forward compatibility. When targeting environments that support namespaces (such as GMRT-only builds in the future), global will affect symbol visibility.

Best practice: Always explicitly declare global when required by the spec.


bind = typed_gml

Controls how a function binds to GML.

[bind = typed_gml]
function add(self, value : int) : unit;

typed_gml enables strict typed wrapper generation.


field

Marks a property as a generated struct field.

[field]
property id : int { get; set; }

Only properties marked [field] are generated.


type_hint

Overrides or clarifies native typing.

[field, type_hint = `uint64`]
property handle : gmval { get; set; }

Used when marshaling needs explicit type control.


🧩 Additional Attributes


hidden

Removes the symbol from GML auto-complete (IntelliSense).

Can be applied to:

  • Functions
  • Properties ([field])
  • Classes

Example:

[hidden]
function __internal_helper() : unit;

Use this for:

  • Internal APIs
  • Low-level utilities
  • Implementation details that should not be used directly

This affects editor experience only — it does not change binding behavior.


start_fn

Marks a function to be called automatically when the game starts.

[start_fn]
function initialize() : unit;

Rules:

  • Only one start_fn is allowed per compilation module.
  • Cannot be used inside a class.
  • Must be a top-level function.

finish_fn

Marks a function to be called automatically when the game finishes.

[finish_fn]
function shutdown() : unit;

Rules:

  • Only one finish_fn is allowed per compilation module.
  • Cannot be used inside a class.
  • Must be a top-level function.

⚠ Attribute Constraints

  • start_fn and finish_fn cannot be applied to class methods.
  • A module cannot declare more than one of each.
  • field only applies to properties.
  • hidden only affects editor visibility.
  • global exists for future namespace-aware targets.

🧠 3. The Most Important Rule: Types

There are two ways to declare types.


✅ A) Normal Types (Direct : type Syntax)

You can write these directly after ::

double
float
int32
uint32
int64
bool
string
object
array
gmval
func
unit (return only)

Example

function add(a: int32, b: int32) : int32;

property name : string;

These are the only types allowed directly.


❗ B) Everything Else MUST Use type_hint

You must use type_hint if the type is:

  • uint8
  • int8
  • uint16
  • int16
  • uint64
  • buffer
  • Custom enum
  • Custom class
  • Optional (?)
  • Collection ([])
  • Fixed-size collection ([X])

The hint must be inside backticks:

type_hint = `...`

🧩 4. Using type_hint

Example: uint64

[field, type_hint = `uint64`]
property channel_id : gmval { get; set; }

Notice:

  • The visible type stays gmval
  • The real type is defined in type_hint

Example: Custom Struct

[field, type_hint = `DiscordUserHandle`]
property user : gmval { get; set; }

Example: Custom Enum

[field, type_hint = `DiscordStatusType`]
property status : gmval { get; set; }

📚 5. Collections (Arrays)

Collections require type_hint.

🔹 Dynamic Array

Use []

[field, type_hint = `uint64[]`]
property participants : gmval { get; set; }

🔹 Fixed Size Array

Use [X]

[field, type_hint = `uint8[16]`]
property key : gmval { get; set; }

❓ 6. Optional Types

Optionals require type_hint.

Use ?

[field, type_hint = `uint64?`]
property parent_id : gmval { get; set; }

You can combine features:

[field, type_hint = `DiscordUserHandle[]?`]
property users : gmval { get; set; }

🧠 7. Functions

Parameters

Parameter types are declared after the parameter name:

function add(a: int32, b: int32) : int32;

If special type required:

function get_user([type_hint = `uint64`] user_id) : gmval;

Return Types

Normal types:

function get_count() : int32;

Custom or advanced return type:

[global, bind = typed_gml, type_hint = `DiscordCall`]
function get_call() : gmval;

⚠ Important Rule

What Where does type_hint go?
Parameter Next to parameter
Return value In function attributes

🏗 8. Classes (Structs)

Classes represent structured objects returned to GML.

They generate:

  • 🧠 Native structs (C++, Java, ObjC, Swift, etc.)
  • 🎮 GML constructors
  • 📦 Optional class-bound functions (GML-side methods backed by native functions)

Required Format

[global]
class `[[StructName]]` prototype Object
{
    [field]
    property property_name : type { get; set; }

    [bind = typed_gml]
    function method_name(self, arg : type) : return_type;
}

🔎 Rules for Classes


1️⃣ Must Have [global]

Classes must always include the global attribute.


2️⃣ Name Must Use Special Syntax

The name must:

  • Be inside backticks
  • Be wrapped in [[ ]]

Example:

class `[[User]]`

This is required for proper symbol resolution.


3️⃣ Prototype Must Always Be Object

prototype Object

This is mandatory.


4️⃣ Properties Must:

  • Use the property keyword
  • Be marked with [field]
  • Include { get; set; }

Example:

[field, type_hint = `uint64`]
property id : gmval { get; set; }

🧩 Class Functions (Method-Like Functions)

Classes may declare functions inside their body.

These are structural GML methods, not native struct methods.

Example:

[global]
class `[[BenchmarkSuite]]` prototype Object
{
    [bind = typed_gml]
    function add(self, fun : func) : unit;

    [bind = typed_gml]
    function remove(self, fun : func) : unit;

    [bind = typed_gml]
    function run_all(self) : unit;

    [bind = typed_gml]
    function report_print(self) : unit;

    [bind = typed_gml]
    function report_as_json(self) : unit;
}

🧠 What Gets Generated

🎮 On the GML Side

The generated constructor for BenchmarkSuite will include these functions as struct members.

That means:

var suite = new BenchmarkSuite();
suite.add(my_func);
suite.run_all();

You do not manually pass self in GML.

The wrapper automatically injects the struct instance as the first argument.


🧩 On the Native Side

Native code does not generate member methods inside the struct.

Instead, extgen generates namespaced free functions with the following pattern:

<ClassName>__<function_name>(const ClassName& self, ...)

Example:

void BenchmarkSuite__add(const BenchmarkSuite& self, GMFunction fun);

Important details:

  • The function name is prefixed with the class name.
  • self is automatically typed as the container class.
  • You do not need to declare a type for self in GMIDL.
  • self is always resolved to the enclosing class type.

🔄 How Binding Works

  1. GML calls:

    suite.add(my_func);
  2. The generated wrapper:

    • Injects suite as self
    • Marshals other parameters
    • Calls the native function:
    BenchmarkSuite__add(self, fun);

This keeps the class clean and lightweight.


💡 Design Philosophy

Class functions are:

  • Structurally attached to GML structs
  • Implemented as free functions natively
  • Function-oriented rather than object-oriented at the native level

This provides:

  • Lightweight marshalling
  • Cleaner ABI surface
  • Lower wire overhead
  • Better performance for handle-based designs

A common pattern is to store only a small native handle inside the struct:

[field]
property handle : uint64 { get; set; }

Then implement all behavior via class functions that operate on that handle.

This keeps:

  • The struct small
  • Native memory ownership controlled
  • The GML interface clean and expressive

⚠ Important Notes

  • Class functions must still use normal GMIDL function syntax.
  • The first parameter must be named self.
  • You do not need to specify a type for self.
  • self will always be resolved as the containing class type.
  • Native structs do not receive member methods.
  • Only properties marked [field] are generated as struct fields.
  • { get; set; } is required for properties.
  • extgen does not support custom getters/setters.

🔠 9. Enums

Enums are simple but follow rules.

Required Format

[global, bind = typed_gml]
enum ActivityActionTypes
{
    Invalid = 0,
    Join = 1,
    JoinRequest = 5,
}

🔎 Rules for Enums

1️⃣ Must Have [global]

Just like classes and functions.


2️⃣ Can Use bind = typed_gml

If you want a strongly-typed GML enum, include:

bind = typed_gml

3️⃣ Underlying Type

You can specify underlying type:

enum ErrorCode : int32
{
    None = 0,
    NetworkError = 1
}

Allowed directly:

int32
uint32
int64

If you need something like uint8, you must use type_hint:

[global, type_hint = `uint8`]
enum SmallEnum
{
    A = 0,
    B = 1
}

4️⃣ Values Must Be Numeric

Enum members must be integer values.


📋 10. Quick Rules Summary

✅ Direct Types Allowed

double
float
int32
uint32
int64
bool
string
object
array
gmval
func
unit

❗ Must Use type_hint For

  • uint8
  • int8
  • uint16
  • int16
  • uint64
  • buffer
  • Custom enums
  • Custom classes
  • Arrays ([])
  • Fixed arrays ([X])
  • Optionals (?)

🧩 Syntax Summary

Feature Syntax
Dynamic array Type[]
Fixed array Type[4]
Optional Type?
Hint required type_hint = `Type`

🎯 Example: Complete Function

[global, bind = typed_gml, type_hint = `DiscordCall`]
function get_call([type_hint = `uint64`] channel_id) : gmval;

What happens:

  • channel_id is uint64
  • Return type is DiscordCall
  • GML receives a typed struct

🎯 Final Takeaway

GMIDL is intentionally strict to keep generation predictable.

Follow these steps:

  1. Use normal types whenever possible.
  2. Use type_hint for everything advanced.
  3. Always use backticks in hints.
  4. Use [], [X], and ? inside hints.
  5. Return type hints go in function attributes.
  6. Parameter hints go next to parameters.
  7. Classes must follow exact naming rules.
  8. Enums must be numeric.

⚠️ 11. Special Types in extgen (Functions & Buffers)

This section describes limitations and runtime behavior specific to extgen.

These rules are important.


🚫 1. Functions and Buffers Cannot Be Struct Members

In extgen:

❌ You cannot:

  • Use func inside a struct/class property
  • Use buffer inside a struct/class property
  • Return func from a function
  • Return buffer from a function

These types are only allowed as function parameters.


🧠 2. Functions (func → GMFunction)

In GMIDL:

function do_something(callback: func) : unit;

In native C++ this becomes:

GMFunction

🔹 Native API

std::uint64_t getId() const;

template<class... Args>
void call(Args&&... args) const;

🔹 How It Works

  • Functions are passed from GML into native code.
  • They are represented as a lightweight handle.
  • You do not need to release them.
  • They are automatically cleaned up when they go out of scope.

🔹 Calling Back Into GameMaker

You call them like this:

callback.call(123, "hello", myStruct);

You can pass:

  • ✅ Numeric scalars
  • ✅ Custom structs
  • ✅ Custom enums
  • std::string
  • std::string_view
  • const char*

🔹 Thread Safety

The call() method is thread-safe.

When called:

  • Data is queued internally
  • Execution happens in GameMaker’s next frame
  • It is safe to call from worker threads

You do not need to handle synchronization.


📦 3. Buffers (buffer → GMBuffer)

In GMIDL:

function process_data([type_hint = `buffer`] data) : unit;

When using buffers:

[type_hint = `buffer`]

In native C++ this becomes:

struct GMBuffer {
private:
    void* ptr;
    std::uint64_t size;

public:
    GMBufferReader getReader();
    GMBufferWriter getWriter();

    void* data() const noexcept;
    std::uint64_t length() const noexcept;
};

🔹 What GMBuffer Represents

It is a raw memory block passed from GameMaker.

You can:

  • Read from it
  • Write to it
  • Access raw pointer

📖 4. Reading From a Buffer

auto reader = buffer.getReader();

You can:

reader.read<T>();
reader.readBytes(ptr, size);

You can safely read:

  • Primitive numeric types
  • Raw bytes ``

✍ 5. Writing To a Buffer

auto writer = buffer.getWriter();

You can:

writer.write<T>(value);
writer.writeBytes(ptr, size);

⚠ Important Buffer Limitation

When writing directly to buffers:

❌ You CANNOT write:

  • Custom structs
  • Custom enums
  • Strings
  • string_view
  • Complex types

You must manually serialize these types into raw bytes.

Buffers are raw memory only.


📱 6. Android Representation

On Android (JNI/Java side):

Buffers are represented as:

java.nio.ByteBuffer

This maps directly to the native memory block.


🎯 Summary

Type Allowed as Parameter Allowed as Struct Field Allowed as Return
func ✅ Yes ❌ No ❌ No
buffer ✅ Yes ❌ No ❌ No

💡 Best Practice

Use:

  • func for async callbacks
  • buffer for binary payloads only
  • Structs for typed data
  • Enums for strongly typed states

Avoid mixing buffers with complex type systems.


🧩 Why This Restriction Exists

extgen is hybrid:

  • Must work in C++
  • Must work in GMRT
  • Must work in legacy runner
  • Must work across all supported platforms

Functions and buffers are runtime primitives, not data structures.

Keeping them parameter-only guarantees:

  • Stable ABI
  • Cross-platform behavior
  • Safe memory ownership

This makes the system predictable and safe across all platforms.

Clone this wiki locally