- Joined
- May 19, 2010
- Messages
- 35
Now that we have unlimited possibilities thanks to memhack it's time to dwelve a bit deeper.
You may have seen this thread by @leandrotp about compiling directly to bytecode.
I'm really into that idea, because it allows for greatly increased performance and unlocks new features (like allocating memory).
The warcraft bytecode (I call it jasm, jass+asm) is not exactly complex or difficult, it's just that there exists pretty much NO documentation what-so-ever. That makes sense, it's not an official feature of Jass.
To get an insight I have written an assembler/disassembler for jasm. The tool can convert between readable jasm and executable bytecode. Also I ported the disassembler code into jass itself. Now you can look at the bytecode of your functions without getting completly mindfucked.
The code to call bytecode from jass and an documented example are in the attached map.
To use my tool either call jasm.exe -h on the command line or drag and drop a file on the included *.bat files. My map dumps the bytecode of the last printed function into a file "CustomMapData/jasm_dump.txt". This file can be converted to jasm by dragging it onto "preload_to_jasm.bat" (there is also "preload dump example.txt" that you can use to try it out). Dragging a *.jasm file onto "jasm_to_array.bat" converts the jasm to a jass function that fills an integer array with the bytecode. You can then simply copy and paste that code into your map.
Sourcecode of jasm.exe is on github.
It looks like this:
Displaying the code of your function works with a simple one-liner:
Arguments that contain only " - " are ignored by the instruction.
Possible types are void, code, int, real, string, handle, bool, int[], real[], string[], hdl[], bool[].
The assembler has a list of all natives and does name lookup. That means &Player is the same as writing F_538.
This table is not complete and may contain errors!
And as a little bonus, dynamic dispatch:
You may have seen this thread by @leandrotp about compiling directly to bytecode.
I'm really into that idea, because it allows for greatly increased performance and unlocks new features (like allocating memory).
The warcraft bytecode (I call it jasm, jass+asm) is not exactly complex or difficult, it's just that there exists pretty much NO documentation what-so-ever. That makes sense, it's not an official feature of Jass.
To get an insight I have written an assembler/disassembler for jasm. The tool can convert between readable jasm and executable bytecode. Also I ported the disassembler code into jass itself. Now you can look at the bytecode of your functions without getting completly mindfucked.
The code to call bytecode from jass and an documented example are in the attached map.
You can find an overview to the jasm instructions further down in this post.
JASS:
scope CustomBytecode initializer init
/*
This example shows how to create a footman in bytecode
To run custom bytecode two textmacros can be used. Both are defined in JasmExecuteUtils.
The first textmacro sets up all necessary functions and globals to run custom bytecode.
It creates 2 functions for us. The name of these function depends on the Parameter to the textmacro.
function ExecuteBytecodeExample takes nothing returns nothing
function GetBytecodeExampleAsCode takes nothing returns code
ExecuteBytecodeExample() runs the bytecode.
GetBytecodeExampleAsCode() returns the bytecode as a code variable that can be used with the native functions like ForGroup
This textmacro also defines an array l__BytecodeExample. That array contains the bytecode that should be executed.
This array can only be used between the call to JasmSetupGlobals and JasmSetupExec
*/
//! runtextmacro JasmSetupGlobals("BytecodeExample")
globals
private integer ID = 'hfoo'
unit created_footman = null
endglobals
// DO NOT TOUCH THIS FUNCTION!
// it's generated bytecode is used to get the internal ids of
// the global variables "ID" and "created_footman"
private function GlobalTable takes nothing returns nothing
set ID = 0
set created_footman = null
endfunction
//! novjass
We want to create a unit and save it to created_footman in bytecode. It should be the equivalent of
set created_footman = CreateUnit(Player(0), ID, 0.0, 0.0, 270.0)
First we need to get a handle to player 0, the call to Player(0)
literal R0 int 0 // Put literal 0 into register R0
push R0 // Push that register on the stack
callnative &Player // Call the native function Player()
// The result of the call to Player is in R0
// Because the first parameter of CreateUnit is the player, we directly push R0 on the stack.
// the stack now contains [player0]
push R0
getvar R0 int &ID // get the global variable ID and put its value into R0. (The variable id of ID is set in l__BytecodeExample[9])
push R0 // push the id on the stack. the stack is now [player0, 'hfoo']
literal R0 real 0.0 // put the real 0.0 into R0
push R0 // push it twice on the stack for the x and y parameter of CreateUnit
push R0 // stack is now [player0, 'hfoo', 0.0, 0.0]
literal R0 real 270.0 // put the real 270.0 into R0 and push it onto the stack
push R0 // stack is now [player0, 'hfoo', 0.0, 0.0, 270.0]
callnative &CreateUnit // Call CreateUnit. This native takes its 5 parameters from the stack and puts its return value int R0
// stack is now []
setvar R0 &created_footman // Write R0 to created_footmap
ret // Return from this function. Will crash Warcraft if this is missing.
//! endnovjass
private function InitJasmArray_l__BytecodeExample takes nothing returns nothing
set l__BytecodeExample[ 0] = 0x0c000400 // literal R0 int - 0
set l__BytecodeExample[ 1] = 0x00000000
set l__BytecodeExample[ 2] = 0x13000000 // push R0 - - -
set l__BytecodeExample[ 3] = 0x00000000
set l__BytecodeExample[ 4] = 0x15000000 // callnative - - - &Player
set l__BytecodeExample[ 5] = 0x00000538
set l__BytecodeExample[ 6] = 0x13000000 // push R0 - - -
set l__BytecodeExample[ 7] = 0x00000000
set l__BytecodeExample[ 8] = 0x0e000400 // getvar R0 int - &ID
set l__BytecodeExample[ 9] = GetGlobalIdCode(function GlobalTable, 0) // get the first variable that is used in the function GlobalTable ("ID")
set l__BytecodeExample[ 10] = 0x13000000 // push R0 - - -
set l__BytecodeExample[ 11] = 0x00000000
set l__BytecodeExample[ 12] = 0x0c000500 // literal R0 real - 0
set l__BytecodeExample[ 13] = 0x00000000
set l__BytecodeExample[ 14] = 0x13000000 // push R0 - - -
set l__BytecodeExample[ 15] = 0x00000000
set l__BytecodeExample[ 16] = 0x13000000 // push R0 - - -
set l__BytecodeExample[ 17] = 0x00000000
set l__BytecodeExample[ 18] = 0x0c000500 // literal R0 real - 270
set l__BytecodeExample[ 19] = 0x43870000
set l__BytecodeExample[ 20] = 0x13000000 // push R0 - - -
set l__BytecodeExample[ 21] = 0x00000000
set l__BytecodeExample[ 22] = 0x15000000 // callnative - - - &CreateUnit
set l__BytecodeExample[ 23] = 0x00000415
set l__BytecodeExample[ 24] = 0x11000000 // setvar R0 - - &created_footman
set l__BytecodeExample[ 25] = GetGlobalIdCode(function GlobalTable, 1)
set l__BytecodeExample[ 26] = 0x27000000 // ret - - - -
set l__BytecodeExample[ 27] = 0x00000000
endfunction
/*
The second textmacro finalizes the generation of the bytecode.
It also creates one function:
function JasmInitBytecodeExample takes nothing returns nothing
This function must be called before ExecuteBytecodeExample() or GetBytecodeExampleCode() can be used
*/
//! runtextmacro JasmSetupExec("BytecodeExample")
private function init takes nothing returns nothing
local trigger trg = CreateTrigger()
// fill array with bytecode
call InitJasmArray_l__BytecodeExample()
// Setup necessary variables to call bytecode
call JasmInitBytecodeExample()
// when player(0) presses escape
call TriggerRegisterPlayerEventEndCinematic(trg, Player(0))
// then execute the bytecode
call TriggerAddAction(trg, GetBytecodeExampleAsCode())
endfunction
endscope
To use my tool either call jasm.exe -h on the command line or drag and drop a file on the included *.bat files. My map dumps the bytecode of the last printed function into a file "CustomMapData/jasm_dump.txt". This file can be converted to jasm by dragging it onto "preload_to_jasm.bat" (there is also "preload dump example.txt" that you can use to try it out). Dragging a *.jasm file onto "jasm_to_array.bat" converts the jasm to a jass function that fills an integer array with the bytecode. You can then simply copy and paste that code into your map.
Sourcecode of jasm.exe is on github.
It looks like this:
Displaying the code of your function works with a simple one-liner:
vJASS:
private function WithIf takes nothing returns integer
if true then
return 1
else
return 2
endif
endfunction
<snip>
//! runtextmacro DumpFunction("WithIf")
Arguments that contain only " - " are ignored by the instruction.
Possible types are void, code, int, real, string, handle, bool, int[], real[], string[], hdl[], bool[].
The assembler has a list of all natives and does name lookup. That means &Player is the same as writing F_538.
This table is not complete and may contain errors!
Name | Id | Arg 1 | Arg 2 | Arg 3 | Arg 4 | Description | Example |
endprogram | 0x01 | - | - | - | - | Used to signal end of parsing. Ignored by the VM | |
jmp_deprecated | 0x02 | - | - | - | label | Behaves exactly like jmp | |
func | 0x03 | type | - | - | function | Used by parser to signal start of a function. Ignored by the VM | func void F_109f |
endf | 0x04 | - | - | - | - | Like func, just for the end | endf |
local | 0x05 | type | - | - | variable | Declares a new local variable | local int V_10a1 |
global | 0x06 | type | - | - | variable | Declares a new global variable | global real V_10a2 |
const | 0x07 | type | - | - | variable | Like global | const handle V_10a3 |
poparg | 0x08 | type | integer | - | variable | Get a function parameter. Arg2 is the number of the parameter, with the rightmost parameter as number 1 | poparg boolean 2 V_10a4 |
cleanstack | 0x0b | integer | - | - | - | Remove Arg1 many parameters from the stack | cleanstack 3 |
literal | 0x0c | register | type | - | integer | Set Arg1 to the value of Arg4 | literal R5 real 3.1415 |
mov | 0x0d | register | register | - | - | Arg1 = Arg2 | mov R0 R5 |
getvar | 0x0e | register | type | - | variable | Copy variable Arg4 to register Arg1 | getvar |
code | 0x0f | register | type | - | function | code | code |
getvar[] | 0x10 | register | register | type | variable | getvar[] | getvar[] |
setvar | 0x11 | register | - | - | variable | Copy register Arg1 to variable Arg4 | setvar R83 V_10a0 |
setvar[] | 0x12 | register | register | - | variable | setvar[] | setvar[] |
push | 0x13 | register | - | - | - | Push Arg1 onto the stack | push R87 |
pop | 0x14 | register | - | - | - | Remove the topmost value of the stack and put it into Arg1 | pop R87 |
callnative | 0x15 | - | - | - | function | Calls a native function. Parameters of that function first need to be pushed | push R1 callnative &Player |
calljass | 0x16 | - | - | - | function | Like callnative but with jass functions. Stack must be cleared afterwards. | push R1 push R2 calljass F_109f cleanstack 2 |
i2r | 0x17 | register | - | - | - | Convert the value in the register to a float | i2r R85 |
and | 0x18 | register | register | register | - | Arg1 = Arg2 and Arg3 Boolean Operation, not bitwise | and R1 R1 R2 |
or | 0x19 | register | register | register | - | Arg1 = Arg2 or Arg3 Boolean Operation, not bitwise | or R1 R1 R2 |
eq | 0x1a | register | register | register | - | If Arg2 and Arg3 are equal then set Arg1 to 1 else to 0 | eq R1 R1 R2 |
ne | 0x1b | register | register | register | - | Not equal, compare eq | ne R1 R1 R2 |
le | 0x1c | register | register | register | - | Less equal, compare eq | le R1 R1 R2 |
ge | 0x1d | register | register | register | - | Greater equal, compare eq | ge R1 R1 R2 |
lt | 0x1e | register | register | register | - | Less than, compare eq | lt R1 R1 R2 |
gt | 0x1f | register | register | register | - | greater than, compare eq | gt R1 R1 R2 |
add | 0x20 | register | register | register | - | Arg1 = Arg2 + Arg3 | add R89 R89 R88 |
sub | 0x21 | register | register | register | - | Arg1 = Arg2 - Arg3 | sub R89 R89 R88 |
mul | 0x22 | register | register | register | - | Arg1 = Arg2 * Arg3 | mul R89 R89 R88 |
div | 0x23 | register | register | register | - | Arg1 = Arg2 / Arg3 | div R89 R89 R88 |
mod | 0x24 | register | register | register | - | Arg1 = Arg2 modulo Arg3 | mod R89 R89 R88 |
neg | 0x25 | register | - | - | - | Arg1 = -Arg1 | neg R39 |
not | 0x26 | register | - | - | - | if Arg1 == 0: Arg1 = 1 else Arg1 = 0 | not R22 |
ret | 0x27 | - | - | - | - | Return from the function. Returnvalue is in R0 | ret |
label | 0x28 | - | - | - | label | Used as a marker for a jump instruction. Ignored by VM | label L_588 |
jmpt | 0x29 | register | - | - | label | Jump if true. Continue evaluation at label Arg4 if Arg1 is true | jmpt L_588 |
jmpf | 0x2a | register | - | - | label | Jump if false, compare jmpt | jmpf L_588 |
jmp | 0x2b | - | - | - | label | Unconditional jump. Continue execution at label Arg4 | jmp L_588 |
And as a little bonus, dynamic dispatch:
vJASS:
function PoorMansJumptable takes nothing returns nothing
call FunA() // 0
return
call FunB() // 1
return
call FunC() // 2
return
call FunD() // 3
return
call FunE() // 4
return
endfunction
function EvalJumptable takes code table, integer num returns nothing
call ForForce(bj_FORCE_PLAYER[0], I2C(C2I(table) + 2 * 8 * num))
endfunction
// 5 functions so i is between 0 and 4 inclusive
function Test takes integer i returns nothing
call EvalJumptable(function PoorMansJumptable, i)
endfunction
Attachments
Last edited: