Cybiko Bytecode Interpreter

Structure And Use of Bytecode Modules

The structure of a module is simple. At the very beginning, there is a table of even number 16-bit words. In other words, there is a word, then some number of pairs of words are followed by one more word. The very first word is an offset to the module entry point: function main(), or whatever you call it; please see the VCC1 compiler documentation for more info on how that compiler handles module entry point (however, your treatment may vary; see below). Pairs are offset (again, relative to the start of the module) from data and code for "exported" objects, respectively (remember — all words, including double words, are stored in BE order!). The very last word is a "terminating NULL". Currently, the following tasks are up to the programmer:

1. How to load the module into memory. You should probably load it like any other resource - from the .app or .dl application archive. You may then keep it in memory (and keep p-code running) until your program terminates, or you may free it upon losing focus (thus effectively suspending p-code execution) and then re-load upon getting focus again.

2. How to interpret data and code pointed to by those offsets at the beginning of the module. For example, Cylandia treats the first offset as the module initialization function's offset (which contains relocation records and objects' ctors), and treats remaining offsets (except for the very last one, of course) as offsets to data and code of game actors. Our C compiler thinks of them as exported structures and respective exported methods.

The only way to execute bytecodes is to call the vm_exec family function found in the bytecode.dl library, like this (below, we assume that entire module got loaded into 'word_t* buff'):

  /* 1) call module initialization routine */ 

  vm_exec( NULL,                                                                     /* no active objects/actors yet */
                (byte_t*) bytecode + 
                bytecode[ bytecode[ I_FIRST_EXPORT ] + I_STARTUP ], /* startup code */
                extension functions );                                               /* functions imported by p-code*/ 

  /* 2) enter main loop */ 

  vm_exec_3( NULL,                                                                  /* no active objects/actors yet */
                (byte_t*) bytecode + 
                bytecode[ bytecode[ I_FIRST_EXPORT ] + I_MAIN ],      /* main(): module entry point */
                extension functions,                                                 /* functions imported by p-code*/
                argc, argv, TRUE);                                                    /* arguments */

  /* 3) call module cleanup routine */ 

  vm_exec( NULL,                                                                     /* no active objects/actors yet */
                (byte_t*) bytecode + 
                bytecode[ bytecode[ I_FIRST_EXPORT ] + I_CLEANUP ], /* cleanup code */
                extension functions );                                              /* functions imported by p-code*/

Important note: all functions that are called via vm_exec() must return with retf opcode (in other words, they're considered far, as opposed to near functions callable via calln.s).

The very last argument to vm_exec() is the address of the table of functions prototyped as follows (also, see callx.b opcode description):

   typedef dword_t (*import_t)( dword_t arg0, dword_t arg1, void*     this_ptr ); 

   import_t extension_functions[] = { /* ... */ }; 

The primary use of extension functions is for implementation of time-critical pieces of code and filling in the gaps which currently exist in the interpreter's import abilities. For example, currently there is no way to import CyOS' global variables, so you'll probably have to provide an extension function which returns the address of a variable (say, a font handle) to get access to it.

Please see the attached example program for further details.