-
Notifications
You must be signed in to change notification settings - Fork 2
user_gmidl_docs
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.
- Basic Structure
- Attributes
- Type Rules
- Using type_hint
- Collections
- Optional Types
- Functions
- Classes (Structs)
- Enums
- Quick Rules Summary
- Special Types: Functions & Buffers
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
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;
Attributes can be:
- ✅ Flags →
global - 🔧 Key/value pairs →
bind = typed_gml - 🧠 Type modifiers →
type_hint = \uint64``
They can be applied to:
- Functions
- Classes
- Properties (
[field]) - Class functions
- Structs
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.
Controls how a function binds to GML.
[bind = typed_gml]
function add(self, value : int32) : unit;
typed_gml enables strict typed wrapper generation.
Marks a property as a generated struct field.
[field]
property id : int32 { get; set; }
Only properties marked [field] are generated.
Overrides or clarifies native typing.
[field, type_hint = `uint64`]
property handle : gmval { get; set; }
Used when marshaling needs explicit type control.
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.
Marks a function to be called automatically when the game starts.
[start_fn]
function initialize() : unit;
Rules:
- Only one
start_fnis allowed per compilation module. - Cannot be used inside a class.
- Must be a top-level function.
Marks a function to be called automatically when the game finishes.
[finish_fn]
function shutdown() : unit;
Rules:
- Only one
finish_fnis allowed per compilation module. - Cannot be used inside a class.
- Must be a top-level function.
-
start_fnandfinish_fncannot be applied to class methods. - A module cannot declare more than one of each.
-
fieldonly applies to properties. -
hiddenonly affects editor visibility. -
globalexists for future namespace-aware targets.
There are two ways to declare types.
You can write these directly after ::
double
float
int32
uint32
int64
bool
string
object
array
gmval
func
unit (return only)
function add(a: int32, b: int32) : int32;
property name : string;
These are the only types allowed directly.
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 = `...`
[field, type_hint = `uint64`]
property channel_id : gmval { get; set; }
Notice:
- The visible type stays
gmval - The real type is defined in
type_hint
[field, type_hint = `DiscordUserHandle`]
property user : gmval { get; set; }
[field, type_hint = `DiscordStatusType`]
property status : gmval { get; set; }
Collections require type_hint.
Use []
[field, type_hint = `uint64[]`]
property participants : gmval { get; set; }
Use [X]
[field, type_hint = `uint8[16]`]
property key : gmval { get; set; }
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; }
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;
Normal types:
function get_count() : int32;
Custom or advanced return type:
[global, bind = typed_gml, type_hint = `DiscordCall`]
function get_call() : gmval;
| What | Where does type_hint go? |
|---|---|
| Parameter | Next to parameter |
| Return value | In function attributes |
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)
[global]
class `[[StructName]]` prototype Object
{
[field]
property property_name : type { get; set; }
[bind = typed_gml]
function method_name(self, arg : type) : return_type;
}
Classes must always include the global attribute.
The name must:
- Be inside backticks
- Be wrapped in
[[ ]]
Example:
class `[[User]]`
This is required for proper symbol resolution.
prototype Object
This is mandatory.
- Use the
propertykeyword - Be marked with
[field] - Include
{ get; set; }
Example:
[field, type_hint = `uint64`]
property id : gmval { get; set; }
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;
}
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.
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.
-
selfis automatically typed as the container class. - You do not need to declare a type for
selfin GMIDL. -
selfis always resolved to the enclosing class type.
-
GML calls:
suite.add(my_func);
-
The generated wrapper:
- Injects
suiteasself - Marshals other parameters
- Calls the native function:
BenchmarkSuite__add(self, fun); - Injects
This keeps the class clean and lightweight.
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
- 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. -
selfwill 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.
Enums are simple but follow rules.
[global, bind = typed_gml]
enum ActivityActionTypes
{
Invalid = 0,
Join = 1,
JoinRequest = 5,
}
Just like classes and functions.
If you want a strongly-typed GML enum, include:
bind = typed_gml
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
}
Enum members must be integer values.
double
float
int32
uint32
int64
bool
string
object
array
gmval
func
unit
- uint8
- int8
- uint16
- int16
- uint64
- buffer
- Custom enums
- Custom classes
- Arrays (
[]) - Fixed arrays (
[X]) - Optionals (
?)
| Feature | Syntax |
|---|---|
| Dynamic array | Type[] |
| Fixed array | Type[4] |
| Optional | Type? |
| Hint required | type_hint = `Type` |
[global, bind = typed_gml, type_hint = `DiscordCall`]
function get_call([type_hint = `uint64`] channel_id) : gmval;
What happens:
-
channel_idis uint64 - Return type is DiscordCall
- GML receives a typed struct
GMIDL is intentionally strict to keep generation predictable.
Follow these steps:
- Use normal types whenever possible.
- Use
type_hintfor everything advanced. - Always use backticks in hints.
- Use
[],[X], and?inside hints. - Return type hints go in function attributes.
- Parameter hints go next to parameters.
- Classes must follow exact naming rules.
- Enums must be numeric.
This section describes limitations and runtime behavior specific to extgen.
These rules are important.
In extgen:
❌ You cannot:
- Use
funcinside a struct/class property - Use
bufferinside a struct/class property - Return
funcfrom a function - Return
bufferfrom a function
These types are only allowed as function parameters.
In GMIDL:
function do_something(callback: func) : unit;
In native C++ this becomes:
GMFunction
std::uint64_t getId() const;
template<class... Args>
void call(Args&&... args) const;- 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.
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*
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.
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;
};It is a raw memory block passed from GameMaker.
You can:
- Read from it
- Write to it
- Access raw pointer
auto reader = buffer.getReader();You can:
reader.read<T>();
reader.readBytes(ptr, size);You can safely read:
- Primitive numeric types
- Raw bytes ``
auto writer = buffer.getWriter();You can:
writer.write<T>(value);
writer.writeBytes(ptr, size);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.
On Android (JNI/Java side):
Buffers are represented as:
java.nio.ByteBuffer
This maps directly to the native memory block.
| Type | Allowed as Parameter | Allowed as Struct Field | Allowed as Return |
|---|---|---|---|
| func | ✅ Yes | ❌ No | ❌ No |
| buffer | ✅ Yes | ❌ No | ❌ No |
Use:
-
funcfor async callbacks -
bufferfor binary payloads only - Structs for typed data
- Enums for strongly typed states
Avoid mixing buffers with complex type systems.
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.
GameMaker 2026