JHCR - A high-level overview

lep

The Problem

Modifying WarCraft 3 maps has a long iteration period due to closing WC3, changing your code and restarting WC3. JHCR (Jass hot code reload) tries to improve this cycle by replacing the slow actions of closing and starting WC3 with a hopefully faster compiler which pushes your code changes directly into an already running WC3.

But how could something like that even work? The answer is multi faceted. So let’s start with some very basic mockups.

The most simple Idea

Let’s say we want to reload a single function, how would that look like in code?

function foo takes nothing returns nothing
    if function_was_reloaded("foo") then
        // somehow execute the new body
        // call BJDebugMsg("New Foo")
    else
        // old body of foo
        call BJDebugMsg("Foo")
    endif
endfunction

While this has many holes it’s conceptually correct already. But for the sake of easy implementation we split up the function into two functiosn, the original function but with a new name and a new function but with the old name:

function JHCR_foo takes nothing returns nothing
    call BJDebugMsg("Foo")
endfunction

function foo takes nothing returns nothing
    if function_was_reloaded("foo") then
        // somehow execute the new body
        // call BJDebugMsg("New Foo")
    else
        call JHCR_foo()
    endif
endfunction

This way we don’t have to change other functions calling foo when we update it. Everything from a simple call-statement over trigger-actions to such things as ExecuteFunc still work just fine.

The harder part is now how to actually execute the new body of foo. To achieve this JHCR provides both a compiler to- and an interpreter for a custom bytecode language which will be superficially introduced in the next section.

The Bytecode

The bytecode was carefully designed to strike a balance between size, parsing speed and executing speed as all of these factors play a big role in our limited WC3 environment.

For example the original foo would be translated to this bytecode:

fun 3 foo
lit string -2 Foo
bind string 1 -2
call -1 2 BJDebugMsg
ret nothing

But this is of course way too verbose and also not very fast to parse. So this format is only used for human consumption; to communicate with WC3 JHCR provides another format for this exact bytecode which looks like this:

| fun  |                       |       bind          |                 |ret|
263.....21136-2.......3.....Foo201361........-2.......22-1.......2.....29139
        |         lit         |                       |     call      |

I’ve annotated the different parts to show the opcode they represent.

This is way easier to parse programatically and takes up less space. Every bytecode uses a fixed number and we encode numbers in a fixed width with added padding to achieve a good tradeoff between size and parsing speed. For fast parsing we use the fact that S2I("123asdf") == S2I(123) so we can move in well defined chunks over the input string.

There are plenty more opcodes ofcourse but we’ll save them for a future post.

Integrating

As we’ve seen previously we took special care to preserve the original name of our function but this was mostly to integrate our system into the maps script. Now we will talk about integrating the maps script into JHCR.

Functions

Take the aboves bytecode as an example: we call the BJ-function BJDebugMsg in there. How do we actually achieve this? As JHCR takes both common.j and Blizzard.j at compile time we assign each function an unique id (BJDebugMsg has id 2 in our example). Now, to be able to call this function we generate a huge if-then-else block for each id at compile time to call out to different functions. If we limit ourself to only three functions, that is DisplayTimedTextToPlayer, BJDebugMsg and foo we can look at the code JHCR generates below.

function JHCR_Auto_call_predefined takes integer JHCR_reg,integer JHCR_i,integer JHCR_ctx returns nothing
    if (JHCR_i < 2) then
        call DisplayTimedTextToPlayer (JHCR_Table_get_player (JHCR_Context_bindings[JHCR_ctx],1),JHCR_Table_get_real (JHCR_Context_bindings[JHCR_ctx],2),JHCR_Table_get_real (JHCR_Context_bindings[JHCR_ctx],3),JHCR_Table_get_real (JHCR_Context_bindings[JHCR_ctx],4),JHCR_Table_get_string (JHCR_Context_bindings[JHCR_ctx],5))
    else
        if (JHCR_i < 3) then
            call BJDebugMsg (JHCR_Table_get_string (JHCR_Context_bindings[JHCR_ctx],1))
        else
            call foo ()
        endif
    endif
endfunction

This is ofcourse quite a mouthfull but it all follows the same pattern. The first parameter is the register the return-value of the called function is stored in. but since all our functions return nothing it wont be used here. The next parameter is the id of the function we want to call. As you can see we do a bunch of comparisons to get to the correct function. The next parameter is a struct called context in which we store a bunch of information for the interpreter, most importantly here the parameters we want passed to the function we want to call. Those parameters are passed via the bind opcode which you can see just before the call opcode in our small example above.

Globals

We also need to be able to read and write globals from our interpreter and we deploy a similar technique as we did for functions. For each available type in JASS (handle, integer, unit, etc.) we generate two functions to read and write a specific global variable of that type. As with functions we assign each global variable a per-type unique id on which we do a bunch of comparisons to set or read the correct value. Let’s again only look at a very small input script with only two global variables bj_MAX_PLAYERS and bj_forLoopAIndex where bj_MAX_PLAYERS is declared constant. JHCR will generate the following functions:

function JHCR_Auto_get_global_integer takes integer JHCR_i returns integer
    if (JHCR_i < 2) then
        return bj_MAX_PLAYERS
    else
        return bj_forLoopAIndex
    endif
endfunction

function JHCR_Auto_set_global_integer takes integer JHCR_i,integer JHCR_v returns nothing
    if (JHCR_i < 2) then
        return 
    else
        set bj_forLoopAIndex = JHCR_v
    endif
endfunction

Note the then-case in JHCR_Auto_set_global_integer: since bj_MAX_PLAYERS is constant we actually can’t set it. But both can of course be read. A similar approach is used for global arrays.

Update

To actually reload changed functions JHCR read the new script file and checks for each function if the hash of the function has changed. If that happens the bytecode like above is created and written to a file to be loaded by the Preload-native. For example the changed foo function would generate the following preload-file:

function foo takes nothing returns nothing
    call BJDebugMsg("New foo")
endfunction

function PreloadFiles takes nothing returns nothing
    call SetPlayerTechMaxAllowed (Player (0),1,1)
    call BlzSetAbilityTooltip ('Agyv',"263.....21136-2.......7.....New foo201361........-2.......22-1.......2.....29139",0)
    call SetPlayerTechMaxAllowed (Player (0),2,0)
endfunction

The generated bytecode is of course very similar to the previously shown since we only changed one string literal after all. Now what happens with this next is topic of a future post. We will end this little post here even though it only scratches the surface of what JHCR has to do. I hope you enjoyed this quick overview of JHCR.

lep . blog