-
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.
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:
globalbind = typed_gmlfieldtype_hint
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.
[global]
class `[[StructName]]` prototype Object
{
[field]
property property_name : type { get; set; }
}
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; }
-
Only properties marked
[field]are generated. -
{ get; set; }is required by the spec. -
extgen does not support custom getters/setters.
-
Classes are:
- 🧠 Native structs in C++
- 🎮 Constructors in GML
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