How to Pass Callback to Dll in RGSS

Way 1: Pure Machine Code

Suppose we want to call the qsort function.

qsort(void *ptr, size_t count, size_t size,
      int (*comp)(const void *, const void *));

We do this in C:

int compare_ints(const void* a, const void* b) {
    int arg1 = *(const int*)a;
    int arg2 = *(const int*)b;

    if (arg1 < arg2) return -1;
    if (arg1 > arg2) return 1;
    return 0;

int ints[] = { -2, 99, 0, -743, 2, INT_MIN, 4 };
int size = sizeof ints / sizeof *ints;

qsort(ints, size, sizeof(int), compare_ints);

What should we do in ruby?

Refer to my previous post, we can just pre-compile the machine code then use it. (This is called AOT in many other applications.)

Got this (binary):

8b44 2404 8b10 8b44 2408 8b00 39c2 7c07
0f9f c00f b6c0 c3b8 ffff ffff c390 9090

Have a test:

compare_ints = [
  %w[8b44 2404 8b10 8b44 2408 8b00 39c2 7c07
     0f9f c00f b6c0 c3b8 ffff ffff c390 9090].join

ints = [ -2, 99, 0, -743, 2, -2147483648, 4 ].pack('l*')
size = 7

dll('msvcrt').qsort(ints, size, 4, compare_ints)
#=> [-2147483648, -743, -2, 0, 2, 4, 99]

This is recommended and fast in most cases. Many useful RGSS scripts are based on pure machine code, such as adding logics in WndProc (by calling SetWindowLong).

Way 2: Wrap Ruby Proc

There is an RGSSEval in rgss*.dll. We can construct a C function with it.

rgss_dllname = "RGSS#{RGSS_VERSION.split('.').join}"
RGSS = api('kernel32', 'GetModuleHandle').call(rgss_dllname)
RGSSEval = api('kernel32', 'GetProcAddress').call(RGSS, 'RGSSEval')
def callback(n = 16, &blk)
  return nil if blk.nil?
  @_cb = "ObjectSpace._id2ref(#{blk.object_id}).call"
  [0x68, @_cb, 0xb8, RGSSEval, 0xd0ff, 0xc204c483, n, 0x9090].pack('CpCLSLSS')

Which does:

0x68 x(long)   : push x = "ObjectSpace._id2ref(#{blk.object_id}).call"
0xb8 p(long)   : eax = p = RGSSEval
0xff 0xd0      : call eax /* i.e. call p */
0x83 0xc4 0x04 : esp += 4 /* balance the stack */
0xc2 0x10 0x00 : ret $16 /* __stdcall */
0x90           : nop

Have a test:

f = callback { p 42 }
api('user32', 'CallWindowProc').call(f, 0, 0, 0, 0)

Looks fine, Huh? Many things to consider:

  1. The GIL will throw errors (CTD) if the callback is async.
  2. Arguments should be carefully translated to ruby version if there is any.
  3. RGSSEval only returns 0 if success, 6 (the value of nil) if error. So passing the result of ruby code to C needs being dealt with.

Similar considerations are handled by the FFI library. (Though we couldn’t use it in RGSS, it’s a good reference for us to do same.)

Way ?: Async to Sync

Here we want to handle the problem of async callbacks. RGSS has only one thread and a natural polling pattern — the Graphics.update loop. We may wrap the async part into sync one — by exporting a GetState function.

Example: AsHttp (Corresponding RGSS script written by me.)

© 2019 hyrious