-
Notifications
You must be signed in to change notification settings - Fork 2
user_gmidl_docs
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, and CMake projects.
- 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), and enums.
Anything inside brackets [...] is an attribute. Attributes modify how extgen generates bindings, wrappers, metadata, and editor behavior.
[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, and structs.
Marks a function or class as globally accessible in GML.
[global]
function do_something() : unit;
Important
GameMaker functions are globally accessible by default. global currently has no enforced namespace effect, but it exists for forward compatibility. When targeting environments that support namespaces (such as future GMRT-only builds), this attribute will affect symbol visibility. Always declare it explicitly when required by spec.
Controls how a function binds to GML. typed_gml enables strict typed wrapper generation.
[bind = typed_gml]
function add(self, value : int32) : unit;
Marks a property as a generated struct field. Only properties marked [field] are generated.
[field]
property id : int32 { get; set; }
Overrides or clarifies native typing. Used when marshaling needs explicit type control.
[field, type_hint = `uint64`]
property handle : gmval { get; set; }
Removes the symbol from GML auto-complete (IntelliSense). Can be applied to functions, properties, and classes. Does not change binding behavior.
[hidden]
function __internal_helper() : unit;
Marks a function to be called automatically when the game starts.
[start_fn]
function initialize() : unit;
Rules: only one start_fn per 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_fn per 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.
These can be written 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;
Use type_hint when the type is:
-
uint8,int8,uint16,int16,uint64 buffer- A custom enum
- A custom class
- Optional (
?) - A collection (
[]) - A fixed-size collection (
[X])
The hint value must be inside backticks:
type_hint = `...`
[field, type_hint = `uint64`]
property channel_id : gmval { get; set; }
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.
[field, type_hint = `uint64[]`]
property participants : gmval { get; set; }
[field, type_hint = `uint8[16]`]
property key : gmval { get; set; }
Optionals require type_hint.
[field, type_hint = `uint64?`]
property parent_id : gmval { get; set; }
Features can be combined:
[field, type_hint = `DiscordUserHandle[]?`]
property users : gmval { get; set; }
function add(a: int32, b: int32) : int32;
When a parameter requires a special type:
function get_user([type_hint = `uint64`] user_id) : gmval;
Normal return type:
function get_count() : int32;
Custom or advanced return type - the type_hint goes in the function attributes:
[global, bind = typed_gml, type_hint = `DiscordCall`]
function get_call() : gmval;
| Location | Where does type_hint go? |
|---|---|
| Parameter | Next to the parameter |
| Return value | In the function attribute list |
Classes represent structured objects returned to GML. They generate native structs (C++, Java, ObjC, Swift), GML constructors, and optional class-bound functions.
[global]
class `[[StructName]]` prototype Object
{
[field]
property property_name : type { get; set; }
[bind = typed_gml]
function method_name(self, arg : type) : return_type;
}
- Must have
[global]. -
Name must use special syntax - inside backticks, wrapped in
[[ ]]:class `[[User]]` - Prototype must be
Object. -
Properties must use the
propertykeyword, be marked[field], and include{ get; set; }.
Classes may declare functions inside their body. These are structural GML methods, not native struct methods.
[global]
class `[[BenchmarkSuite]]` prototype Object
{
[bind = typed_gml]
function add(self, fun : func) : unit;
[bind = typed_gml]
function run_all(self) : unit;
}
On the GML side, the generated constructor includes these functions as struct members:
var suite = new BenchmarkSuite();
suite.add(my_func);
suite.run_all();self is not passed manually in GML - the wrapper injects the struct instance as the first argument.
On the native side, extgen generates namespaced free functions:
void BenchmarkSuite__add(const BenchmarkSuite& self, GMFunction fun);
void BenchmarkSuite__run_all(const BenchmarkSuite& self);- The function name is prefixed with the class name.
-
selfis automatically typed as the containing class. - You do not declare a type for
selfin GMIDL.
Class functions use a free-function pattern at the native level. This keeps the ABI surface lightweight and works well with handle-based designs:
[field]
property handle : uint64 { get; set; }
Store a small native handle in the struct and implement all behavior via class functions that operate on that handle. This keeps native memory ownership controlled and the GML interface clean.
- Class functions must use normal GMIDL function syntax.
- The first parameter must be named
self. -
selfis resolved as the containing class type - no explicit type needed. - Native structs do not receive member methods.
- Only properties marked
[field]are generated as struct fields. -
{ get; set; }is required for all properties. - Custom getters/setters are not supported.
[global, bind = typed_gml]
enum ActivityActionTypes
{
Invalid = 0,
Join = 1,
JoinRequest = 5,
}
- Must have
[global]. -
bind = typed_gmlproduces a strongly-typed GML enum. - Underlying type can be specified directly for
int32,uint32,int64:For other types (e.g.enum ErrorCode : int32 { None = 0, NetworkError = 1 }uint8), usetype_hint:[global, type_hint = `uint8`] enum SmallEnum { A = 0, B = 1 } - Enum member values must be numeric integers.
double float int32 uint32 int64 bool string object array gmval func unit
-
uint8,int8,uint16,int16,uint64 buffer- Custom enums and 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;
-
channel_idisuint64 - Return type is
DiscordCall - GML receives a typed struct
func and buffer are only allowed as function parameters. They cannot be:
- Struct/class property types
- Function return types
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 passed from GML are represented as lightweight handles. They are cleaned up automatically when they go out of scope - no manual release needed.
Calling back into GameMaker:
callback.call(123, "hello", myStruct);Accepted argument types: numeric scalars, custom structs, custom enums, std::string, std::string_view, const char*.
Thread safety: call() is thread-safe. Data is queued internally and executed in GameMaker's next frame. Safe to call from worker threads.
function process_data([type_hint = `buffer`] data) : unit;
In native C++:
struct GMBuffer {
void* data() const noexcept;
std::uint64_t length() const noexcept;
GMBufferReader getReader();
GMBufferWriter getWriter();
};Reading:
auto reader = buffer.getReader();
reader.read<T>();
reader.readBytes(ptr, size);Writing:
auto writer = buffer.getWriter();
writer.write<T>(value);
writer.writeBytes(ptr, size);Important
Buffers are raw memory. You cannot write custom structs, enums, strings, or other complex types directly. Serialize them to raw bytes first.
On Android (JNI): buffers are represented as java.nio.ByteBuffer.
| Type | As parameter | As struct field | As return value |
|---|---|---|---|
func |
✅ Yes | ❌ No | ❌ No |
buffer |
✅ Yes | ❌ No | ❌ No |
Use func for async callbacks, buffer for binary payloads, structs for typed data, and enums for strongly typed states.
This restriction ensures a stable ABI and consistent cross-platform behavior (C++, GMRT, legacy runner, all supported platforms).
GameMaker 2026