hxcpp
uses a conservative stop-the-world GC, where the threads need to co-operate.
hx::GCEnterBlocking()
/ gc_enter_blocking()
/ cpp.vm.Gc.enterGCFreeZone()
from Haxe before a potentially blocking system call (filesystem, network, etc).hx::GCExitBlocking()
/ gc_exit_blocking()
/ cpp.vm.Gc.exitGCFreeZone()
from Haxe before making more GC calls.When you create a thread from Haxe, it starts attached. Before a non-Haxe created thread can interact with hxcpp
, some care must be taken, since GC allocations are done using a GC context per thread, and all threads must respect the stopped world.
SetTopOfStack(int * inTop,bool inPush)
- inTop
- pointer to top of stack to attach, or 0
to remove stack.
- inPush
- usually true
. Recursive attachment/detachment.emscripten
which goes up - but still use same terminology/picture, just change the less-thans to greater-thans in code.Say the system starts each program stack at 10000, the stack might look like this, with local variables and arguments pushed on the stack:
10000
-----------------------------------------------
9996 startup temp variable
9992 startup temp variable
-- main function --
9988 main return address - order and details of this are ABI specific
9984 char ** argv
9980 int argc
hxcpp
then runs the main code, which starts with the macro HX_TOP_OF_STACK
, which expands to something like:
int t0 = 99;
hx::SetTopOfStack(&t0,false);
...
__boot_all();
__hxcpp_main();
-- main function --
9988 main return address order and details of this are ABI specific
9984 char ** argv
9980 int argc
9976 int t0
-- hx::SetTopOfStack --
records '9976' as top of stack for this thread
Later, many generated functions deep, __hxcpp_main
generates an allocation call which triggers a collection.
...
8100 Array<Bullet> bullets
-- alloc Enemy --
...
-- Call collect --
8050 int bottomOfStackTemp
MarkConservative(&bottomOfStackTemp, 9976) -> scans stack from 8050 -> 9976
MarkConservative(Capture registers)
Enter/exit use a similar technique, where the registers are captured and the bottomOfStack
is 'locked-in' when the "enter GC-free zone" call is made.
8100 Array<Bullet> bullets
-- EnterGCFreeZone --
8088 int bottomOfStackTemp
thread->setBottomOfStack(&bottomOfStackTemp)
thread->captureRegisters()
return
* any changes here will not affect GC
Now, when another thread performs a collection, the GC-free thread can be scanned from 8088 to 9976, regardless of any stuff happening lower down the stack.
Top of stack can be tricky to get right when a GUI framework does not really have a "main".
10000
-----------------------------------------------
9996 startup temp variable
9992 startup temp variable
-- main function --
setupWindows(onReadyCallback)......
...
8000
-- onReadyCallback --
7976 int t0
SetTopOfStack(&t0,false) -> 7966
__hxcpp_main();
setOnFrameCallack(haxeOnFrame)
return;
Later, the haxeOnFrame
callback is triggered, but not "below" __hxcpp_main
9800 -- haxeOnFrame ---
// Top of stack will be below bottom of stack.
Solutions:
inForce = false
- gc_set_top_of_stack(void * inTopOfStack,bool inForce);
hxcpp_main
and reattach each callback
- Android solution because render callbacks happen on different threads
- gc_set_top_of_stack(&base,true);
- attach
- gc_set_top_of_stack(0,true);
- detachhxcpp
will check for calls from unattached threads.hxcpp
can log conservative ranges. With a native debugger you can check the address of your local variables and ensure they are included.hxcpp
will scan native objects on the stack, but will not follow non-Haxe pointers to other objects, so additional GC roots may be required.