-
Notifications
You must be signed in to change notification settings - Fork 3
VM Documentation
Native methods are created by loading Lua scripts which fill the correct method references with the functions given. Native methods can be used to expose ComputerCraft hardware components, give access to the file system, expose fast Lua algorithms, and more.
Java classes which contain natives must explicitly call System.load
to call a native Lua script which can create its native methods. There are examples of native scripts inside the package java.io.lang.native
from the runtime library.
To create a native method, you should create a script and register it with the VM like so:
MyNatives.lua
natives["foo.bar.MyNatives"] = natives["foo.bar.MyNatives"] or { }
Next, you can structure any method like this:
natives[ --[[ fully qualified class name ]] ][ --[[ method signature ]] ] = function( --[[ arguments ]] )
--[[ body ]]
end
As an example, here is a method for repeating a string a number of times.
MyNatives.java
package foo.bar;
Note here that the path given to ``System.load`` is relative to the class path.
public MyNatives {
static {
System.load("somewhere/MyNatives.lua")
}
public static native String repeat();
}
MyNatives.lua
natives["foo.bar.MyNative"]["repeat(java/lang/String;I)java/lang/String;"] = function(toRep, n)
local luaStr = toLString(toRep)
local luaRep = luaStr:rep(n)
local javaStr = toJString(luaRep)
return javaStr
end
The Java Runtime Library
The class loader parses information out of a compiled class binary and stores it for later use in the translator. It makes a single pass through the binary to get information and store it in a table It is also responsible for creating the method references which will later call to the translator at runtime. Aside from that, classloader.lua
contains important constants specific to information stored in Java class binaries.
Our class loader is able to read from both raw class files and from class files stored in JARs.
JVML-JIT is a Java VM which features just-in-time compilation of Java bytecode to Lua bytecode. The translator is the part of the VM which takes some Java bytecode and turns it into computationally equivalent Lua bytecode as needed. The translation happens for one Java method at a time, generating a single Lua function for each. The generated code should try to take advantage of the fact that compilation occurs at runtime, and optimize on the fact that the JVM and Lua VM operate differently. For example, Java bytecode has no way to index stack values directly, but Lua stores the stack as indexed registers. Because of this, the pop instruction requires no generation of Lua bytecode at all.
The translator lives in a file called newjit.lua
.
We have created a convenient framework for generating Lua bytecode called asm.lua. Its job is to open a stream for instructions to be constructed and hold information about the code we've assembled at any point. It saves every instruction entered thus far, holds information about the assumed state of registers, keeps an index of how far up the stack the stream has reached to allocate registers, and keeps track of how many Lua constants have been used in the constant pool.
asm.lua is crucial to the translator, but it only contains general usage which would be suitable for any application. easm.lua is our own extension to asm.lua which adds useful operations such as automatic register tracking, asm constructs for frequently used operations like throwing exceptions, a runtime information table for passing information to methods, jump translation, and others.
Here is an example from newjit.lua which generates code for the add
opcode.
end, function() -- 62
-- add
local r1 = stream.peek(1)
local r2 = stream.peek(0)
stream.ADD(r1, r1, r2)
stream.free(1)
end, function() -- 63
Let's take this line-by-line.
local r1 = stream.peek(1)
local r2 = stream.peek(0)
Peek is the stack operation which gets a value from a place relative to the top of the stack, the last place where values were added. Here we give it 1
and 0
to indicate we want the registers one away from the top and directly at the top respectively. r1
and r2
now hold a register index each.
stream.ADD(r1, r1, r2)
The ALL_CAPS functions inside stream objects are instruction generation functions, which always add exactly one Lua instruction to the stream. ADD
generates the ADD
opcode in Lua just JMP
generates a JMP
opcode. The parameters are ra
, rb
, rc
where after the ADD
is run ra
will hold the value of rb + rc
.
stream.free(1)
This frees a register from the stack, letting the stream know that it can now be allocated in the future. We have to do this whenever we are finished using a register to prevent the operand stack from using up all our registers.
It is very important to look at the optimization opportunities of the generated bytecode. Currently, only a few optimizations are done which require a single pass, but it is entirely possible to do more complex things such as method inlining, hotspot analyzation, loop unrolling, translation of loops to use Lua's for-loop opcode, and more.