Run Heaps games
on Apple Silicon.
Ash runs Heaps HashLink bytecode natively on Apple Silicon. Use matching Heaps and hlsdl sources, and place the HDLLs beside your .hl file. Ash provides the shared HashLink runtime.
b58e23e4f631f3edcc0041fd4c8ecb89df43afe5db6f0f4846d5b544ce33cdca. It uses LC_RPATH @loader_path, preventing a copied HDLL from reaching back into the machine that built it and loading a second HashLink runtime.Run Heaps games
on Windows.
Ash runs Heaps HashLink bytecode on Windows x86_64 with HashLink's own prebuilt HDLLs. Place them beside your .hl file along with the DLLs they import. Ash provides the shared HashLink runtime.
fmt.hdll, ui.hdll, uv.hdll, and sdl.hdll from a HashLink win64 release archive. Copy any required DLL dependencies from the same archive, including SDL3.dll and OpenAL32.dll.Match the externs to the native libraries
The Haxe externs determine which native symbols appear in game.hl. Pair current Heaps with HashLink's hlsdl source; mixing an older hlsdl checkout with a newer sdl.hdll produces unresolved or mismatched symbols.
$ haxelib git heaps https://github.com/HeapsIO/heaps.git $ haxelib git hlsdl https://github.com/HaxeFoundation/hashlink.git master libs/sdl $ haxelib path heaps $ haxelib path hlsdl
Compile to HashLink bytecode
-lib heaps -lib hlsdl -main Main -hl bin/game.hl
$ haxe compile.hxml $ cp /path/to/sdl.hdll bin/
Place the required native libraries in the same directory as game.hl. Common dependencies are sdl.hdll, fmt.hdll, ui.hdll, and uv.hdll.
Start with the hybrid runtime
$ ash --mode hybrid bin/game.hl
[ash] Loading HDLL-compatible ash_std at .../libhl.dylib
[ash] Loading HDLL: sdl from "bin/sdl.hdll"
The first log line confirms that Ash and the HDLLs share a garbage collector and HashLink type globals. No DYLD_LIBRARY_PATH is required. Ash's macOS release places both libhl.dylib and its libhl.1.dylib alias beside the executable.
Main.main() runs as soon as bytecode and HDLL loading complete. hxd.App.init() runs later, after Heaps creates its window, event loop, engine, and graphics context. In the Base2D example the measured gap was about 1.39 seconds. Do not add sleeps, pause the sentinel, or manually drive hxd.System.mainLoop(); those workarounds create an artificial delay.Package the runtime and libraries
The Ash binary includes its standard library, but HDLLs need a shared runtime on disk. They import hl_* symbols from @rpath/libhl.dylib, which dyld loads separately. Include Ash's shared runtime when packaging a game that uses HDLLs.
libhl.dylib or libhl.1.dylib. Upstream HashLink uses the versioned name.lipo -archs on every HDLL.libhl creates separate garbage collectors and can cause memory corruption.$ cp libash_std.dylib game/libhl.dylib $ cp libash_std.dylib game/libhl.1.dylib $ install_name_tool -id @rpath/libhl.dylib game/libhl.dylib $ install_name_tool -id @rpath/libhl.1.dylib game/libhl.1.dylib # point each HDLL at that runtime, and let @rpath mean "next to me" $ install_name_tool -change /abs/path/libash_std.dylib \ @rpath/libhl.dylib game/mylib.hdll $ install_name_tool -add_rpath @loader_path game/mylib.hdll # clear quarantine BEFORE signing, or the signature covers a modified file $ xattr -cr game/ $ for f in game/*.hdll game/*.dylib; do codesign --force -s - "$f"; done
$ lipo -archs game/*.hdll $ otool -L game/*.hdll | grep -E 'libhl|libash_std' $ codesign -v game/*.hdll
An rpath outside the game directory can resolve to another runtime. Check LC_RPATH entries with otool -l and use @loader_path for the packaged libraries.
Troubleshoot loading and crashes
Check for a second libhl instance. glCreateShader may succeed, with the crash occurring when hlsdl boxes the returned ID. Use the linked relocatable HDLL and check that the compatibility-runtime log line appears before any HDLL load.
Your Haxe hlsdl externs and native sdl.hdll are from different revisions. Recompile game.hl after selecting hlsdl 1.17.
Update Ash. Older macOS builds mishandled a bare bytecode filename such as ash game.hl and selected the static runtime before discovering HDLLs in the current directory.
Use a current Ash build. Its HashLink callback compatibility layer preserves safe defaults when hlsdl supplies null wrapper callbacks; the Base2D example is tested through repeated minimize/restore cycles.
The library may be present but blocked by an invalid signature. Re-sign the affected file with codesign --force -s -, followed by its path.
Check the signature and quarantine attributes. Clear quarantine on the affected files with xattr -cr, then re-sign them.
Not a macOS binary, or the wrong architecture. Some Haxe library releases ship a Windows DLL under the .hdll name. Check with file x.hdll — expect Mach-O … arm64.
The bytecode imports an HDLL that is not in the directory. Ash requires it at startup even when the feature goes unused, so stage it or rebuild the bytecode without that library.
Two runtime instances, usually an rpath resolving @rpath/libhl.dylib to a copy outside the game directory. Two libhl images mean two garbage collectors managing the same objects.
fmt.hdll built against shared libpng and libjpeg needs those dylibs staged too; one that links them statically does not. otool -L tells you which you have.Diagnose performance problems
Set these environment variables before starting Ash. They are read once at startup.
ASH_GC_STATS=1 — logs the pause duration, freed blocks, and live set for each collection. Start here when investigating stutter. Pauses around 50 ms can cause visible hitches and audio interruptions.ASH_GC_HEAP_MB=n — heap cap, defaulting to a share of machine RAM. Raise it if allocation fails.ASH_PROFILE=phases — reports elapsed time for each execution phase.ASH_TIER1_PROBE=1 — logs each compilation with a function index and timestamp. Use the timings to investigate compilation stalls.ASH_WORKERS=n — scheduler threads, defaulting to cores minus one.ASH_GC_STRESS=1 — runs frequent collections to help reproduce GC rooting bugs. Expect a substantial slowdown.ASH_LIBHL=system|embedded — overrides runtime selection when you suspect the wrong libhl.sample <pid> while the game is stalled and include it in the report. The thread stacks help distinguish a GC pause, compilation, and a lock wait.Match the externs to the native libraries
The Haxe externs determine which native symbols appear in game.hl. Pair current Heaps with HashLink's hlsdl source, and take the compiled HDLLs from a HashLink release of the same generation; older externs against a newer sdl.hdll produce unresolved or mismatched symbols.
> haxelib git heaps https://github.com/HeapsIO/heaps.git > haxelib git hlsdl https://github.com/HaxeFoundation/hashlink.git master libs/sdl > haxelib path heaps > haxelib path hlsdl
Install Ash with PowerShell:
> irm https://ash.rayzor.tech/install.ps1 | iex
That puts ash.exe in ~\.ash\bin and adds the directory to your user PATH. The HashLink runtime travels with it — libhl.dll and the libhl.1.dll alias that HashLink 1.x builds import — so there is nothing further to install, and nothing to copy per game.
Compile to HashLink bytecode
-lib heaps -lib hlsdl -main Main -hl bin/game.hl
> haxe compile.hxml > copy hashlink-win64\*.hdll bin\
Place the required HDLLs beside game.hl, along with their DLL dependencies. Common HDLLs include sdl.hdll, fmt.hdll, ui.hdll, and uv.hdll. SDL needs SDL3.dll; OpenAL needs OpenAL32.dll. These DLLs are included in the HashLink release archive.
Start with the hybrid runtime
> ash --mode hybrid bin\game.hl
[ash] Loading HDLL-compatible ash_std at ...\libhl.dll
[ash] Loading HDLL: sdl from "bin\sdl.hdll"
The first log line confirms that Ash and the HDLLs share a garbage collector and HashLink type globals. Nothing needs adding to PATH for the runtime itself — Ash's Windows release places libhl.dll and its libhl.1.dll alias beside the executable, and the loader searches that directory first.
Main.main() runs as soon as bytecode and HDLL loading complete. hxd.App.init() runs later, after Heaps creates its window, event loop, engine, and graphics context. Do not add sleeps or manually drive hxd.System.mainLoop(); those workarounds create an artificial delay.ui.Sentinel is disabled. HashLink's watchdog modifies the watched thread's instruction pointer, which is incompatible with Ash's JIT frames. Ash returns null from hl_thread_start for this request, so the program runs without the watchdog.Package the game libraries
On a machine with Ash installed, HDLLs use the libhl.dll beside ash.exe. Package your game with its required HDLLs and their DLL dependencies. Leave the runtime in the Ash installation directory.
bin\ game.hl sdl.hdll fmt.hdll ui.hdll uv.hdll SDL3.dll <- found because it sits beside sdl.hdll OpenAL32.dll <- only if the program uses openal.hdll
Ash loads HDLLs with LOAD_WITH_ALTERED_SEARCH_PATH, so a DLL beside the HDLL is found first. Windows would otherwise start its search at the executable's directory and never look in the HDLL's own.
libhl.dll and its libhl.1.dll alias beside ash.exe. Keep copies out of the game directory: loading a second runtime creates a separate garbage collector and can corrupt memory.SDL3.dll and, if needed, OpenAL32.dll from the HashLink archive into the game directory.install.ps1 places Ash and its runtime in ~\.ash\bin and adds the directory to PATH. That installation can run multiple games.Troubleshoot loading and crashes
The HDLL imports a symbol that the runtime does not export. Windows rejects the library before running its code, but this error does not identify the missing symbol. Update Ash first. If the error persists, check whether the HDLL requires a HashLink ABI that Ash does not yet support.
Check for missing DLL dependencies beside the HDLL. sdl.hdll requires SDL3.dll, and openal.hdll requires OpenAL32.dll.
Not a Windows binary, or the wrong architecture — some Haxe library releases ship a macOS or Linux build under the .hdll name. Ash names the format it actually found in its own error, ahead of the loader's text.
Your Haxe hlsdl externs and native sdl.hdll are from different revisions. Recompile game.hl against externs matching the HDLL you staged.
Ash chose its built-in runtime, which leaves the HDLLs bound to a second copy whose collector was never started. It selects the shared one when it finds HDLLs beside the bytecode — check they are in that directory, and that libhl.dll sits beside ash.exe.
The bytecode imports an HDLL that is not in the directory. Ash requires it at startup even when the feature goes unused, so stage it or rebuild the bytecode without that library.
Check whether a HashLink libhl.dll is being loaded before Ash's copy. Two runtime instances can leave separate garbage collectors managing the same objects, causing crashes later in execution.
fmt.hdll built against shared libpng and libjpeg needs those DLLs staged too; one that links them statically does not. Check the HDLL's import table to see which DLLs it needs.Diagnose performance problems
Set these environment variables before starting Ash. They are read once at startup.
ASH_GC_STATS=1 — logs the pause duration, freed blocks, and live set for each collection. Start here when investigating stutter. Pauses around 50 ms can cause visible hitches and audio interruptions.ASH_GC_HEAP_MB=n — heap cap, defaulting to a share of machine RAM. Raise it if allocation fails.ASH_PROFILE=phases — reports elapsed time for each execution phase. The sampling profiler is macOS and Linux only; this one works everywhere.ASH_TIER1_PROBE=1 — logs each compilation with a function index and timestamp. Use the timings to investigate compilation stalls.ASH_WORKERS=n — scheduler threads, defaulting to cores minus one.ASH_GC_STRESS=1 — runs frequent collections to help reproduce GC rooting bugs. Expect a substantial slowdown.ASH_STD_LINKAGE=dynamic — forces the shared runtime even with no HDLLs present, to A/B against the built-in one.Working example: examples/heaps_base2d
← Back to Ash