Skip to content

user_gmidl_docs

DiasFranciscoA edited this page Feb 17, 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.

Example:

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

Attributes can be:

  • ✅ Flags → global
  • 🔧 Key/value → bind = typed_gml
  • 🧠 Type modifiers → type_hint = \...``

Most commonly used attributes:

  • global
  • bind = typed_gml
  • field
  • type_hint

🧠 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