12.6.6 Threads And Stacks

Conservative, co-operation

hxcpp uses a conservative stop-the-world GC, where the threads need to co-operate.

  • Threads must not change GC pointers in the collection phase.
  • The thread stacks/registers must be scanned for GC pointers.
  • Threads must not block without letting the GC system know not to wait for them, otherwise GC blocks until the end of block.
  • Call hx::GCEnterBlocking() / gc_enter_blocking() / cpp.vm.Gc.enterGCFreeZone() from Haxe before a potentially blocking system call (filesystem, network, etc).
  • Call hx::GCExitBlocking() / gc_exit_blocking() / cpp.vm.Gc.exitGCFreeZone() from Haxe before making more GC calls.
  • Might need to pre-allocate buffers.
  • Don't forget to exit blocking on error conditions.
Foreign Threads

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.

  • Foreign threads must be attached-detached - SetTopOfStack(int * inTop,bool inPush) - inTop - pointer to top of stack to attach, or 0 to remove stack. - inPush - usually true. Recursive attachment/detachment.
  • Must not change things when the world is stopped.
  • Must define their stack range for scanning.
  • If you are attached, you may need to enter/exit GC-free zone.
  • Must release context when done, if no more calls are going to be made.
  • Make sure local variables are covered in the stack. - Compiler may reorder, so be careful.
  • Read documentation because some things, e.g. audio callbacks, happen on other threads.
  • You can use other techniques, e.g.: - Create a Haxe thread, which blocks waiting for a signal. - Foreign thread generates request and signals Haxe thread. - Haxe thread performs job and generates data then signals foreign thread. - Foreign thread picks up data and carries on.
Top of Stack
  • To understand how to handle threads, you need a mental picture of the C++ stack.
  • The stack usually grows "down". That is, if the first stack location is 10000, the next one will be 9999 etc.
  • Historical, but consistent. Except for 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.

Not Called From Main

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:

  • Make sure you get in at top of main - may scan too much?
  • Ratchet up top-of-stack in callbacks, inForce = false - gc_set_top_of_stack(void * inTopOfStack,bool inForce);
  • Detach main thread after 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); - detach
Debugging.
  • In debug mode, hxcpp 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.