Thanks to my further advancements[?] in technology, some of my extensions can be used in even older GameMaker versions, such as GameMaker 8.1.
Context
I've been making GameMaker extensions for slightly more than a decade now.
Some are small, others are big;
Some do incredibly niche things, others have been popular enough that
their ideas or code have been adopted in GameMaker itself
("legacy curiosities"
list now contains over a dozen items).
Over time I made certain improvements to my extension-making process.
GmxGen
To create a GameMaker extension, you write your code in one or other language (depending on platform), compile it (if appropriate), add the file(s) to your GameMaker project, and tell GameMaker what functions the files define and how they should be called.
The last part means doing data entry in a little editor that looks like this:
Extension editor in GameMaker (LTS)
So you have your function name (shown to the user), external name (as defined in the code), help line (shown in auto-completion), and types for each argument (if needed, more on this later).
And that's how it goes with native functions in most languages, but I couldn't shake the feeling that something could have been better - GameMaker cannot know this, but my C++ functions are always written like this, for example:
dllx double fun_1(double num, const char* str) { ... }
That's pretty non-ambiguous as to what types the arguments and return values have, and if you were to look at the source code, you could immediately tell what you should be filling out.
So I made GmxGen, a tool that looks at your source files and populates a GameMaker extension with definitions for obviously exported functions.
As a tool that looks at code, it's not aware of macros, type aliases, or other cool tricks that your language might have, but also it doesn't care whether what you're giving it is the real code, so you can do simple preprocessing before giving it the code and it'll still work.
After GameMaker Studio 2 released, I expanded GmxGen a little to also have "file sync" functionality so that I could make and test extensions in GMS2 and then auto-generate a GM:S extension for anyone that hasn't moved versions yet.
And when GameMaker Studio 2.3 released, featuring a variety of novelties and a few breaking changes, I added a small thing to GmxGen so that I could have code like this:
// GMS >= 2.3: buffer_set_surface(buffer, surface, offset); /*/ buffer_set_surface(buffer, surface, 0, offset, 0); //*/
and the opening //
would get flipped to /*
when copying the file to the
GM:S and GMS2.2 projects:
/* GMS >= 2.3: buffer_set_surface(buffer, surface, offset); /*/ buffer_set_surface(buffer, surface, 0, offset, 0); //*/
This way I could target three major GameMaker versions from a single codebase.
Over time I added various other bits to GmxGen and it remains to be used for pretty much all of my extensions.
GmlCppExtFuncs
For the longest time, GameMaker's extension system had some limitations - for example, GameMaker had support for 64-bit integers since 2013 or so, but you couldn't pass it to native extensions because the supported types are "doubles" (64-bit floats) and strings (or, well, pointers).
If your extension needed these, the common practice was to make a GML script that splits the number into two 32-bit halves (which are safe to pass as a float) or re-assembles a number from two halves. This worked, but was a little janky.
I later came up with an alternative: you could put your numbers in a buffer and pass its address to the extension for reading, like so:
var b = global._my_ext_buffer; buffer_seek(b, buffer_seek_start, 0); buffer_write(b, buffer_u64, v1); // note: called u64, but GM's 64-bit integers are signed buffer_write(b, buffer_u64, v2); my_dll_function(buffer_get_address(b));
and then in your C++ code you could set up the function like
dllx double my_dll_function(int64_t* values) { auto v1 = values[0]; auto v2 = values[0]; ... }
to read them.
After using this in a few extensions, I have established that this was a pretty good way to pass various values back and forth between GameMaker and C++ code, but also that writing this sort of boilerplate wasn't such a good time.
So I made GmlCppExtFuncs, a tool that finds tagged functions in your C++ code and generates GmxGen-compatible wrapper functions for them, so
dllg int64_t ext_add_int64s(int64_t a, int64_t b) { return a + b; }
will get a C++ helper like this:
extern int64_t ext_add_int64s(int64_t a, int64_t b); dllx double ext_add_int64s_raw(void* _inout_ptr) { gml_istream _in(_inout_ptr); int64_t _arg_a = _in.read<int64_t>(); int64_t _arg_b = _in.read<int64_t>(); int64_t _ret = ext_add_int64s(_arg_a, _arg_b); gml_ostream _out(_inout_ptr); _out.write<int64_t>(_ret); return 1; }
and a GML helper like this:
/// ext_add_int64s(a:int, b:int)->int var _buf = ext_prepare_buffer(16); buffer_write(_buf, buffer_u64, argument0); buffer_write(_buf, buffer_u64, argument1); if (ext_add_int64s_raw(buffer_get_address(_buf), 16)) { buffer_seek(_buf, buffer_seek_start, 0); return buffer_read(_buf, buffer_u64); } else return undefined;
And this worked really well!
An extension wrapping one or other C++ SDK was now slightly more than a list of SDK functions adjusted for GameMaker naming conventions.
Sometime around 2022, GameMaker introduced a new form of extension functions that can work with GML values directly, which is good for some cases, but still requires more boilerplate than it did with my approach.
GameMaker 8.1
In 2016, I made Steamworks.gmk for a client, a Steamworks SDK wrapper that worked in GM8.1.
Along with it, I made a simple
GMS1 ➜ GM8 extension converter,
which would convert an .extension.gmx
to a set of GM8.x scripts doing
external_define
and external_call
s respectively.
It was pretty limited, but I was able to use it for 2[?] other extensions over years.
Earlier this year, I've had an idea: some of my extensions don't do anything special beyond a few WinAPI calls, so I should be able to make GM8.1 versions of them relatively easily, right?
Well, almost:
- GM8.1 didn't have buffers
(I did remember this one) - But also GM8.1 didn't have pointers
Even if you made a to-spec implementation of GM:S buffers in a DLL, you wouldn't be able to pass a buffer address to other DLLs as a pointer. - Functions that return pointers in GM:S (e.g.
window_handle
) return numbers.
This doesn't sound bad, but passing a number in place of a string will turn it into a string, so instead of a pointer the extension would get a string with a pointer in it as a number, which is undesirable.
And that would rule out most of the candidates without doing one or other hack!
Having contemplated the situation, I settled on a workaround: GmlCppExtFuncs can generate a minimal set of buffer function replicas for auto-generated GML code to interact with the DLL, and the DLL can use that buffer instead of a buffer address when this happens.
And thus, the earlier GML helper would become something like:
/// ext_add_int64s(a:int, b:int)->int var _buf = external_call(global.f_ext_gmkb_prepare, 16); external_call(global.f_ext_gmkb_write_u64, argument0); external_call(global.f_ext_gmkb_write_u64, argument1); if (external_call(global.f_ext_add_int64s_raw, "", 16)) { external_call(global.f_ext_gmkb_rewind); return external_call(global.f_ext_gmkb_read_u64); } else return undefined;
Evidently, this would still be limited to double
s instead of real 64-bit integers,
but also most 64-bit integers in affected DLLs are pointers or HWND
s
(which are 32-bit in 32-bit applications), so it works out!
What extensions?
Since I will definitely forget to update the list of I include it in this post I made an itch.io "collection" containing the extensions that now have a 8.1 version (and notes if anyone tested them on other versions).
Here's what you can expect over time:
- Little WinAPI extensions from Curiosities
- Mouselock
- Possibly a few more things that I forgot because they're only on GitHub
Answering the inevitable question, I don't plan making a version of GMLive for 8.1, but you could make your own based on gmk-snippets if you figure out how to quickly get code out of a GM81 file (or if you find a GM version where the project files are easier to read).
Supported GM versions
The extensions are tested in GameMaker 8.1.141 because that's the only version that you can still activate at all (by which I mean, you can't right now).
The extensions may work in older GameMaker versions with some luck,
but your mileage may vary (e.g. argument[]
array didn't exist before 8.1
so anything with optional arguments will need tweaking).
The extensions may work in versions between GM8.1 and GameMaker: Studio (such as GM:HTML5), though I cannot easily tell which is the first version that can use GM:S-format extensions - try that first.
Rest assured, using these on anything other than GM8.1 is at your own risk.
Installing extensions
- Copy the DLL(s) to your project folder
(or add them to your Included Files) - Import the scripts
(by dragging-and-dropping the.gml
file onto GameMaker) - If there's a file ending on
constants.txt
, import constants from it
(using menu:Resources➜Define Constants)
Most extensions have an explicit init script (usually called extension_name_init()
).
If there isn't one, extension_name_init_dll()
will load the DLL and init the functions.
Differences
Figuring out a way to auto-generate adjusted documentation would exceed the (non-existing) budget for this experiment, so here's a rundown of automatic sacrifices:
- Arrays are replaced by ds_lists (can't pass an array in/out of a function in GM8.1)
In cases where no list would be provided/returned,-1
is used instead ofundefined
.
Don't forget tods_list_destroy
the lists returned from the DLL functions! - Utility functions that rely on buffers or surface->buffer conversions are not available because there are no buffers and no fast way to grab pixels from a surface.
Check individual extension pages for any other differences/limitations.
Technical support
GameMaker versions prior to GM:S 1.3 (~2014) did not have a debugger in a conventional sense, so my ability to troubleshoot GM8.1-specific issues with extensions is limited.
If you're using these GameMaker versions in 2024, you can probably understand.
Conclusions
This has been an interesting challenge to tackle, and we'll see whether anyone will use these apart of the few people that were already enthusiastic about this when I started making this.
Thanks for reading!