From 35d02b287efa5c68413cbe0010c433dfb243c652 Mon Sep 17 00:00:00 2001 From: Shu Lei Date: Wed, 3 Oct 2007 17:39:38 +0000 Subject: [PATCH] added debugging support and integrated with Eclipse debugger --- src/main/java/lua/LuaJVM.java | 311 ---------- src/main/java/lua/value/LBoolean.java | 38 ++ src/main/java/lua/value/LDouble.java | 119 ++++ src/main/java/lua/value/LFunction.java | 33 ++ src/main/java/lua/value/LInteger.java | 90 +++ src/main/java/lua/value/LNil.java | 23 + src/main/java/lua/value/LString.java | 392 +++++++++++++ src/main/java/lua/value/LTable.java | 540 ++++++++++++++++++ src/main/java/lua/value/LThread.java | 13 + src/main/java/lua/value/LUserData.java | 33 ++ .../java/lua/debug/DebugStackStateTest.java | 56 ++ 11 files changed, 1337 insertions(+), 311 deletions(-) delete mode 100644 src/main/java/lua/LuaJVM.java create mode 100644 src/main/java/lua/value/LBoolean.java create mode 100644 src/main/java/lua/value/LDouble.java create mode 100644 src/main/java/lua/value/LFunction.java create mode 100644 src/main/java/lua/value/LInteger.java create mode 100644 src/main/java/lua/value/LNil.java create mode 100644 src/main/java/lua/value/LString.java create mode 100644 src/main/java/lua/value/LTable.java create mode 100644 src/main/java/lua/value/LThread.java create mode 100644 src/main/java/lua/value/LUserData.java create mode 100644 src/test/java/lua/debug/DebugStackStateTest.java diff --git a/src/main/java/lua/LuaJVM.java b/src/main/java/lua/LuaJVM.java deleted file mode 100644 index f9008c89..00000000 --- a/src/main/java/lua/LuaJVM.java +++ /dev/null @@ -1,311 +0,0 @@ -/******************************************************************************* -* Copyright (c) 2007 LuaJ. All rights reserved. -* -* Permission is hereby granted, free of charge, to any person obtaining a copy -* of this software and associated documentation files (the "Software"), to deal -* in the Software without restriction, including without limitation the rights -* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -* copies of the Software, and to permit persons to whom the Software is -* furnished to do so, subject to the following conditions: -* -* The above copyright notice and this permission notice shall be included in -* all copies or substantial portions of the Software. -* -* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -* THE SOFTWARE. -******************************************************************************/ -package lua; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.util.Timer; -import java.util.TimerTask; - -import lua.addon.luacompat.LuaCompat; -import lua.addon.luajava.LuaJava; -import lua.debug.DebugEvent; -import lua.debug.DebugEventType; -import lua.debug.DebugRequest; -import lua.debug.DebugRequestListener; -import lua.debug.DebugResponse; -import lua.debug.DebugStackState; -import lua.debug.DebugSupport; -import lua.debug.DebugUtils; -import lua.io.Closure; -import lua.io.LoadState; -import lua.io.Proto; -import lua.value.LString; -import lua.value.LValue; - -import org.apache.commons.cli.CommandLine; -import org.apache.commons.cli.CommandLineParser; -import org.apache.commons.cli.GnuParser; -import org.apache.commons.cli.HelpFormatter; -import org.apache.commons.cli.OptionBuilder; -import org.apache.commons.cli.Options; -import org.apache.commons.cli.ParseException; - -/** - * LuaJVM executes a lua program in normal run mode or debug mode. - * - * @author: Shu Lei - * @version: 1.0 - */ -public class LuaJVM implements DebugRequestListener { - protected Options options = new Options(); - protected boolean isDebugMode = false; - protected DebugSupport debugSupport; - protected int requestPort; - protected int eventPort; - protected String script; - protected String[] scriptArgs; - protected StackState state; - protected boolean isReady = false; - protected boolean isTerminated = false; - - @SuppressWarnings("static-access") - public LuaJVM() { - options.addOption(OptionBuilder.withArgName("requestPort eventPort"). - hasArgs(2). - isRequired(false). - withValueSeparator(' '). - withDescription("run LuaJ VM in debug mode"). - create("debug")); - options.addOption(OptionBuilder.withArgName("LuaJProgram"). - withDescription("lua program to be executed"). - isRequired(). - hasArgs(). - withValueSeparator(' '). - create("file")); - } - - protected void printUsage() { - HelpFormatter formatter = new HelpFormatter(); - formatter.printHelp("java LuaJVM", options); - } - - protected void parse(String[] args) throws ParseException { - CommandLineParser parser = new GnuParser(); - try { - CommandLine line = parser.parse(options, args); - if (line.hasOption("debug")) { - this.isDebugMode = true; - String[] ports = line.getOptionValues("debug"); - - this.requestPort = Integer.parseInt(ports[0]); - if (this.requestPort <= 0) { - throw new ParseException("Invalid request port: it must be greater than zero."); - } - - this.eventPort = Integer.parseInt(ports[1]); - if (this.eventPort <= 0) { - throw new ParseException("Invalid event port: it must be greater than zero."); - } - - if (this.requestPort == this.eventPort) { - throw new ParseException("Invalid ports: request port and event port must be different"); - } - } - - if (line.hasOption("file")) { - String[] fileArgs = line.getOptionValues("file"); - this.script = URLDecoder.decode(fileArgs[0], "UTF-8"); - DebugUtils.println("Lua script to run: " + this.script); - this.scriptArgs = new String[fileArgs.length - 1]; - for (int i = 1; i < fileArgs.length; i++) { - this.scriptArgs[i-1] = URLDecoder.decode(fileArgs[i], "UTF-8"); - } - } - } catch(NumberFormatException e) { - throw new ParseException("Invalid port number: " + e.getMessage()); - } catch (UnsupportedEncodingException e) { - throw new ParseException("Malformed program argument strings: " + e.getMessage()); - } - } - - protected boolean isDebug() { - return this.isDebugMode; - } - - protected int getRequestPort() { - return this.requestPort; - } - - protected int getEventPort() { - return this.eventPort; - } - - protected String getScript() { - return this.script; - } - - protected boolean hasScriptArgs() { - return (this.scriptArgs != null && this.scriptArgs.length > 0); - } - - protected String[] getScriptArgs() { - return this.scriptArgs; - } - - public void run() throws IOException { - if (isDebug()) { - doDebug(); - } else { - doRun(); - } - } - - protected void init() { - // reset global states - GlobalState.resetGlobals(); - - // add LuaJava bindings - LuaJava.install(); - - // add LuaCompat bindings - LuaCompat.install(); - } - - public void doRun() throws IOException { - init(); - - // new lua state - state = new StackState(); - - // convert args to lua - int numOfScriptArgs = getScriptArgs().length; - LValue[] vargs = new LValue[numOfScriptArgs]; - for (int i = 0; i < numOfScriptArgs; i++) { - vargs[i] = new LString(getScriptArgs()[i]); - } - - // load the Lua file - DebugUtils.println("loading Lua script '" + getScript() + "'"); - InputStream is = new FileInputStream(new File(getScript())); - Proto p = LoadState.undump(state, is, getScript()); - - // create closure and execute - Closure c = new Closure(state, p); - state.doCall(c, vargs); - } - - private void doDebug() throws IOException { - DebugUtils.println("start debugging..."); - this.debugSupport = new DebugSupport(this, getRequestPort(), getEventPort()); - DebugUtils.println("created client request socket connection..."); - debugSupport.start(); - - DebugUtils.println("setting up LuaJava and debug stack state..."); - - init(); - - // new lua state - state = new DebugStackState(); - getDebugState().addDebugEventListener(debugSupport); - - // load the Lua file - DebugUtils.println("loading Lua script '" + getScript() + "'"); - InputStream is = new FileInputStream(new File(getScript())); - Proto p = LoadState.undump(state, is, getScript()); - - // create closure and execute - final Closure c = new Closure(state, p); - getDebugState().suspend(); - - new Thread(new Runnable() { - public void run() { - int numOfScriptArgs = getScriptArgs().length; - LValue[] vargs = new LValue[numOfScriptArgs]; - for (int i = 0; i < numOfScriptArgs; i++) { - vargs[i] = new LString(getScriptArgs()[i]); - } - - getDebugState().doCall(c, vargs); - stop(); - } - }).start(); - - debugSupport.fireEvent(new DebugEvent(DebugEventType.started)); - } - - private DebugStackState getDebugState() { - return (DebugStackState)state; - } - - /* (non-Javadoc) - * @see lua.debug.DebugRequestListener#handleRequest(java.lang.String) - */ - public DebugResponse handleRequest(DebugRequest request) { - if (!isDebug()) { - throw new UnsupportedOperationException("Must be in debug mode to handle the debug requests"); - } - - DebugUtils.println("handling request: " + request.toString()); - switch (request.getType()) { - case suspend: - DebugResponse status = getDebugState().handleRequest(request); - DebugEvent event = new DebugEvent(DebugEventType.suspendedByClient); - debugSupport.fireEvent(event); - return status; - case resume: - status = getDebugState().handleRequest(request); - event = new DebugEvent(DebugEventType.resumedByClient); - debugSupport.fireEvent(event); - return status; - case exit: - stop(); - default: - return getDebugState().handleRequest(request); - } - } - - protected void stop() { - DebugUtils.println("exit LuaJ VM..."); - if (this.debugSupport != null) { - DebugEvent event = new DebugEvent(DebugEventType.terminated); - debugSupport.fireEvent(event); - Timer timer = new Timer("DebugServerDeathThread"); - timer.schedule(new TimerTask() { - public void run() { - debugSupport.stop(); - debugSupport = null; - } - }, 500); - } - getDebugState().exit(); - } - - /** - * Parses the command line arguments and executes/debugs the lua program. - * @param args -- command line arguments: - * [-debug requestPort eventPort] -file luaProgram args - * @throws IOException - */ - public static void main(String[] args) { - LuaJVM vm = new LuaJVM(); - - try { - vm.parse(args); - } catch (ParseException e) { - DebugUtils.println(e.getMessage()); - vm.printUsage(); - return; - } - - try { - vm.run(); - } catch (IOException e) { - //TODO: handle the error - e.printStackTrace(); - } - } -} diff --git a/src/main/java/lua/value/LBoolean.java b/src/main/java/lua/value/LBoolean.java new file mode 100644 index 00000000..2971a655 --- /dev/null +++ b/src/main/java/lua/value/LBoolean.java @@ -0,0 +1,38 @@ +package lua.value; + +public final class LBoolean extends LValue { + + public static final LBoolean TRUE = new LBoolean("true",true); + + public static final LBoolean FALSE = new LBoolean("false",false); + + public static final LString TYPE_NAME = new LString(Type.bool.toString()); + + private final LString m_name; + private final boolean m_value; + + private LBoolean( String name, boolean value ) { + this.m_name = new LString( name ); + this.m_value = value; + } + + public final LString luaAsString() { + return m_name; + } + + public final boolean luaAsBoolean() { + return m_value; + } + + public final int luaAsInt() { + return m_value? 1: 0; + } + + public final static LBoolean valueOf(boolean value) { + return value? TRUE: FALSE; + } + + public LString luaGetType() { + return TYPE_NAME; + } +} diff --git a/src/main/java/lua/value/LDouble.java b/src/main/java/lua/value/LDouble.java new file mode 100644 index 00000000..6d8acb03 --- /dev/null +++ b/src/main/java/lua/value/LDouble.java @@ -0,0 +1,119 @@ +package lua.value; + +import lua.Lua; + +public class LDouble extends LNumber { + + private final double m_value; + + public LDouble(double value) { + this.m_value = value; + } + + public int hashCode() { + return (int) m_value; + } + + public LString luaAsString() { + long l = (long) m_value; + if ( m_value == (double) l ) { + // TODO: is this a good idea? + return new LString( Long.toString( l ) ); + } else { + return LString.valueOf( m_value ); + } + } + + public boolean isInteger() { + // Cast to int and then back to double and see if the value + // survives the round trip. + return ( (double) ( (int) m_value ) ) == m_value; + } + + // binary operations on integers, first dispatch + public LValue luaBinOpUnknown(int opcode, LValue lhs) { + return lhs.luaBinOpDouble( opcode, this.m_value ); + } + + // binary operations on mixtures of doubles and integers + public LValue luaBinOpInteger(int opcode, int rhs) { + return luaBinOpDoubleDouble( opcode, m_value, (double) rhs ); + } + + // binary operations on doubles + public LValue luaBinOpDouble(int opcode, double rhs) { + return luaBinOpDoubleDouble( opcode, m_value, rhs ); + } + + public static LValue luaBinOpDoubleDouble( int opcode, double lhs, double rhs ) { + switch ( opcode ) { + case Lua.OP_ADD: return new LDouble( lhs + rhs ); + case Lua.OP_SUB: return new LDouble( lhs - rhs ); + case Lua.OP_MUL: return new LDouble( lhs * rhs ); + case Lua.OP_DIV: return new LDouble( lhs / rhs ); + case Lua.OP_MOD: return new LDouble( lhs - Math.floor(lhs/rhs) * rhs ); + // case Lua.OP_POW: return new LDouble( dpow(lhs, rhs) ); + } + return luaUnsupportedOperation(); + } + + /* + public static double dpow(double a, double b) { + if ( b < 0 ) + return 1 / dpow( a, -b ); + int p = 1; + int whole = (int) b; + for ( double v=a; whole > 0; whole>>=1, v=v*v ) + if ( (whole & 1) != 0 ) + p *= v; + int frac = (int) (0x10000 * b); + for ( ; (frac&0xffff)!=0; frac<<=1 ) { + a = Math.sqrt(a); + if ( (frac & 0x8000) != 0 ) + p *= a; + } + return p; + } + */ + + + public int luaAsInt() { + return (int) m_value; + } + + public double luaAsDouble() { + return m_value; + } + + // binary compares on integers, first dispatch + public boolean luaBinCmpUnknown(int opcode, LValue lhs) { + return lhs.luaBinCmpDouble( opcode, this.m_value ); + } + + // binary compares on mixtures of doubles and integers + public boolean luaBinCmpInteger(int opcode, int rhs) { + return luaBinCmpDoubleDouble( opcode, m_value, (double) rhs ); + } + + // binary compares on doubles + public boolean luaBinCmpDouble(int opcode, double rhs) { + return luaBinCmpDoubleDouble( opcode, m_value, rhs ); + } + + // compare two doubles + public static boolean luaBinCmpDoubleDouble( int opcode, double lhs, double rhs ) { + switch ( opcode ) { + case Lua.OP_EQ: return lhs == rhs; + case Lua.OP_LT: return lhs < rhs; + case Lua.OP_LE: return lhs <= rhs; + } + luaUnsupportedOperation(); + return false; + } + + /** Arithmetic negative */ + public LValue luaUnaryMinus() { + return new LDouble( -m_value ); + } + +} diff --git a/src/main/java/lua/value/LFunction.java b/src/main/java/lua/value/LFunction.java new file mode 100644 index 00000000..7a13359e --- /dev/null +++ b/src/main/java/lua/value/LFunction.java @@ -0,0 +1,33 @@ +package lua.value; + +import lua.VM; + + +public class LFunction extends LValue { + + public static final LString TYPE_NAME = new LString(Type.function.toString()); + + public LString luaAsString() { + return new LString( "function: "+hashCode() ); + } + + public void luaSetTable(VM vm, LValue table, LValue key, LValue val) { + vm.push( this ); + vm.push( table ); + vm.push( key ); + vm.push( val ); + vm.lua_call( 3, 0 ); + } + + public void luaGetTable(VM vm, LValue table, LValue key) { + vm.push( this ); + vm.push( table ); + vm.push( key ); + vm.lua_call( 2, 1 ); + } + + public LString luaGetType() { + return TYPE_NAME; + } + +} diff --git a/src/main/java/lua/value/LInteger.java b/src/main/java/lua/value/LInteger.java new file mode 100644 index 00000000..a17ae1af --- /dev/null +++ b/src/main/java/lua/value/LInteger.java @@ -0,0 +1,90 @@ +package lua.value; + +import lua.Lua; + +public class LInteger extends LNumber { + private final int m_value; + + public LInteger(int value) { + this.m_value = value; + } + + public final int hashCode() { + return hashCodeOf( m_value ); + } + + public static int hashCodeOf( int v ) { + return v; + } + + public int luaAsInt() { + return m_value; + } + + public LString luaAsString() { + return LString.valueOf(m_value); + } + + public boolean isInteger() { + return true; + } + + // binary operations on integers, first dispatch + public LValue luaBinOpUnknown(int opcode, LValue lhs) { + return lhs.luaBinOpInteger( opcode, this.m_value ); + } + + // binary operations on integers + public LValue luaBinOpInteger(int opcode, int rhs) { + switch ( opcode ) { + case Lua.OP_ADD: return new LInteger( m_value + rhs ); + case Lua.OP_SUB: return new LInteger( m_value - rhs ); + case Lua.OP_MUL: return new LInteger( m_value * rhs ); + case Lua.OP_DIV: return new LInteger( m_value / rhs ); + case Lua.OP_MOD: return new LInteger( m_value - ((int) Math.floor(m_value/(double)rhs)) * rhs ); + case Lua.OP_POW: return new LInteger( ipow(m_value, rhs) ); + } + return luaUnsupportedOperation(); + } + + private static int ipow(int v, int rhs) { + int p = 1; + for ( ; rhs > 0; rhs>>=1, v=v*v ) + if ( (rhs & 1) != 0 ) + p *= v; + return p; + } + + // binary operations on mixed integer, double + public LValue luaBinOpDouble(int opcode, double rhs) { + return LDouble.luaBinOpDoubleDouble(opcode, (double) m_value, rhs ); + } + + // binary compare for integers, first dispatch + public boolean luaBinCmpUnknown(int opcode, LValue lhs) { + return lhs.luaBinCmpInteger( opcode, this.m_value ); + } + + // unsupported except for numbers + public boolean luaBinCmpInteger(int opcode, int rhs) { + switch ( opcode ) { + case Lua.OP_EQ: return m_value == rhs; + case Lua.OP_LT: return m_value < rhs; + case Lua.OP_LE: return m_value <= rhs; + } + luaUnsupportedOperation(); + return false; + } + + // unsupported except for numbers + public boolean luaBinCmpDouble(int opcode, double rhs) { + return LDouble.luaBinCmpDoubleDouble(opcode, (double) m_value, rhs ); + } + + /** Arithmetic negative */ + public LValue luaUnaryMinus() { + return new LInteger( -m_value ); + } + + +} diff --git a/src/main/java/lua/value/LNil.java b/src/main/java/lua/value/LNil.java new file mode 100644 index 00000000..7ef0dd5f --- /dev/null +++ b/src/main/java/lua/value/LNil.java @@ -0,0 +1,23 @@ +package lua.value; + +public final class LNil extends LValue { + public static final LNil NIL = new LNil(); + public static final LString TYPE_NAME = new LString(Type.nil.toString()); + + public final LString luaAsString() { + return TYPE_NAME; + } + + public boolean luaAsBoolean() { + return false; + } + + public LString luaGetType() { + return TYPE_NAME; + } + + public int luaAsInt() { + return 0; + } + +} diff --git a/src/main/java/lua/value/LString.java b/src/main/java/lua/value/LString.java new file mode 100644 index 00000000..4ef285d2 --- /dev/null +++ b/src/main/java/lua/value/LString.java @@ -0,0 +1,392 @@ +package lua.value; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; + +import lua.Lua; + +/** + * A String implementation for Lua using bytes instead of chars. + * + * This should have the following advantages: + * + * (1) We can use strings as byte buffers, as Lua does, and therefore avoid + * questions about how to adapt Lua APIs that use strings with binary data. + * + * (2) Half the memory usage when strings are primarily ASCII + * + * + * TODO: Decide if/when to copy the bytes to a new array to ensure memory does + * not "leak" in the form of unused portions of byte arrays. Currently, for + * efficiency, new LStrings and substrings never create copies. + */ +public class LString extends LValue { + + public static final LString TYPE_NAME = new LString(Type.string.toString()); + + public final byte[] m_bytes; + public final int m_offset; + public final int m_length; + public final int m_hash; + + private static LTable s_stringMT; + + /** + * Construct a Lua string from the given Java string. Characters are encoded + * using UTF-8. + */ + public LString(String string) { + byte[] bytes; + try { + bytes = string.getBytes( "UTF-8" ); + } catch ( UnsupportedEncodingException exn ) { + bytes = stringToUtf8Bytes( string ); + } + this.m_bytes = bytes; + this.m_offset = 0; + this.m_length = m_bytes.length; + this.m_hash = hashBytes( m_bytes, 0, m_length ); + } + + /** + * Construct a string from the given byte array. + * + * new LString(b) is identical to new LString(b, 0, b.length) + */ + public LString(byte[] bytes) { + this( bytes, 0, bytes.length ); + } + + /** + * Construct a string from the given byte array and range. For efficiency, + * the byte array is not copied. Lua strings are immutable so the bytes must + * not be modified after the string is constructed. + */ + public LString(byte[] bytes, int off, int len) { + if ( off < 0 || len < 0 || off+len > bytes.length ) + throw new IndexOutOfBoundsException(); + this.m_bytes = bytes; + this.m_offset = off; + this.m_length = len; + this.m_hash = hashBytes( bytes, off, len ); + } + + public boolean equals(Object o) { + if ( o != null && o instanceof LString ) { + LString s = (LString) o; + return m_hash == s.m_hash && + m_length == s.m_length && + ( ( m_bytes == s.m_bytes && m_offset == s.m_offset ) || + equals( m_bytes, m_offset, s.m_bytes, s.m_offset, m_length ) ); + } + return false; + } + + public int compareTo( LString o ) { + final byte[] a = this.m_bytes; + final byte[] b = o.m_bytes; + int i = this.m_offset; + int j = o.m_offset; + final int imax = i + m_length; + final int jmax = j + o.m_length; + + if ( a == b && i == j && imax == jmax ) + return 0; + + while ( i < imax && j < jmax ) { + if ( a[i] != b[i] ) { + return ( ( (int)a[i] ) & 0x0FF ) - ( ( (int)b[j] ) & 0x0FF ); + } + i++; + j++; + } + + return m_length - o.m_length; + } + + public int hashCode() { + return m_hash; + } + + public int length() { + return m_length; + } + + public LString substring( int beginIndex, int endIndex ) { + return new LString( m_bytes, m_offset + beginIndex, endIndex - beginIndex ); + } + + public int charAt( int index ) { + if ( index < 0 || index >= m_length ) + throw new IndexOutOfBoundsException(); + return luaByte( index ); + } + + /** Java version of strpbrk, which is a terribly named C function. */ + public int indexOfAny( LString accept ) { + final int ilimit = m_offset + m_length; + final int jlimit = accept.m_offset + accept.m_length; + for ( int i = m_offset; i < ilimit; ++i ) { + for ( int j = accept.m_offset; j < jlimit; ++j ) { + if ( m_bytes[i] == accept.m_bytes[j] ) { + return i - m_offset; + } + } + } + return -1; + } + + public int indexOf( LString s, int start ) { + final int slen = s.length(); + final int limit = m_offset + m_length - slen; + for ( int i = m_offset + start; i <= limit; ++i ) { + if ( equals( m_bytes, i, s.m_bytes, s.m_offset, slen ) ) { + return i; + } + } + return -1; + } + + public static LString valueOf( double d ) { + return new LString( String.valueOf( d ) ); + } + + public static LString valueOf( int x ) { + return new LString( String.valueOf( x ) ); + } + + public static LString concat( final LString[] strings ) { + int length = 0; + for ( int i = 0; i < strings.length; ++i ) { + length += strings[i].length(); + } + byte[] bytes = new byte[length]; + + for ( int i = 0, offset = 0; i < strings.length; ++i ) { + LString s = strings[i]; + final int len = s.length(); + System.arraycopy( s.m_bytes, s.m_offset, bytes, offset, len ); + offset += len; + } + + return new LString( bytes ); + } + + /** + * Write the specified substring of this string to the given output stream. + */ + public void write( OutputStream os, int offset, int len ) throws IOException { + if ( offset < 0 || len < 0 ) + throw new IndexOutOfBoundsException(); + if ( offset + len > m_length ) + throw new IndexOutOfBoundsException(); + + os.write( m_bytes, m_offset+offset, len ); + } + + public void write(OutputStream os) throws IOException { + write(os, 0, m_length); + } + + /** + * Copy the bytes of the string into the given byte array. + */ + public void copyInto( int strOffset, byte[] bytes, int arrayOffset, int len ) { + System.arraycopy( m_bytes, m_offset+strOffset, bytes, arrayOffset, len ); + } + + /** + * Produce an InputStream instance from which the bytes of this LString can be read. + * Underlying byte array is not copied. + */ + public ByteArrayInputStream toInputStream() { + // Well, this is really something. + // Javadoc for java versions 1.3 and earlier states that if reset() is + // called on a ByteArrayInputStream constructed with the 3-argument + // constructor, then bytes 0 .. offset will be returned by the next + // calls to read(). In JDK 1.4, the behavior improved, so that the + // initial mark is set to the initial offset. We still need to + // override ByteArrayInputStream here just in case we run on a + // JVM with the older behavior. + return new ByteArrayInputStream( m_bytes, m_offset, m_length ) { + public synchronized void reset() { + pos = Math.max( m_offset, mark ); + } + }; + } + + public boolean luaBinCmpUnknown(int opcode, LValue lhs) { + return lhs.luaBinCmpString(opcode, this); + } + + public boolean luaBinCmpString(int opcode, LString rhs) { + switch ( opcode ) { + case Lua.OP_EQ: return equals(rhs); + case Lua.OP_LT: return compareTo(rhs) < 0; + case Lua.OP_LE: return compareTo(rhs) <= 0; + } + luaUnsupportedOperation(); + return false; + } + + public LValue luaBinOpDouble( int opcode, double m_value ) { + return luaToNumber().luaBinOpDouble( opcode, m_value ); + } + + public LValue luaBinOpInteger( int opcode, int m_value ) { + return luaToNumber().luaBinOpInteger( opcode, m_value ); + } + + public LValue luaBinOpUnknown( int opcode, LValue lhs ) { + return luaToNumber().luaBinOpUnknown( opcode, lhs ); + } + + public LValue luaUnaryMinus() { + return luaToNumber().luaUnaryMinus(); + } + + public LValue luaToNumber() { + return luaToNumber( 10 ); + } + + public LValue luaToNumber( int base ) { + if ( base >= 2 && base <= 36 ) { + String str = toJavaString().trim(); + try { + return new LInteger( Integer.parseInt( str, base ) ); + } catch ( NumberFormatException nfe ) { + if ( base == 10 ) { + try { + return new LDouble( Double.parseDouble( str ) ); + } catch ( NumberFormatException nfe2 ) { + } + } + } + } + + return LNil.NIL; + } + + public LString luaAsString() { + return this; + } + + public String toJavaString() { + try { + return new String( m_bytes, m_offset, m_length, "UTF-8" ); + } catch ( UnsupportedEncodingException uee ) { + throw new RuntimeException("toJavaString: UTF-8 decoding not implemented"); + } + } + + /** Built-in opcode LEN, for Strings and Tables */ + public LValue luaLength() { + return new LInteger( length() ); + } + + public LString luaGetType() { + return TYPE_NAME; + } + + public LTable luaGetMetatable() { + synchronized ( LString.class ) { + return s_stringMT; + } + } + + /** + * Get the metatable for all string values. Creates the table if it does not + * exist yet, and sets its __index entry to point to itself. + * + * @return metatable that will be used for all strings + */ + public static synchronized LTable getMetatable() { + if ( s_stringMT == null ) { + s_stringMT = new LTable(); + s_stringMT.put( TM_INDEX, s_stringMT ); + } + return s_stringMT; + } + + public static boolean equals( LString a, int i, LString b, int j, int n ) { + return equals( a.m_bytes, a.m_offset + i, b.m_bytes, b.m_offset + j, n ); + } + + public static boolean equals( byte[] a, int i, byte[] b, int j, int n ) { + if ( a.length < i + n || b.length < j + n ) + return false; + final int imax = i + n; + final int jmax = j + n; + while ( i < imax && j < jmax ) { + if ( a[i++] != b[j++] ) + return false; + } + return true; + } + + private static int hashBytes( byte[] bytes, int offset, int length ) { + // Compute the hash of the given bytes. + // This code comes right out of Lua 5.1.2 (translated from C to Java) + int h = length; /* seed */ + int step = (length>>5)+1; /* if string is too long, don't hash all its chars */ + for (int l1=length; l1>=step; l1-=step) /* compute hash */ + h = h ^ ((h<<5)+(h>>2)+(((int) bytes[offset+l1-1] ) & 0x0FF )); + return h; + } + + private static byte[] stringToUtf8Bytes( final String string ) { + final int strlen = string.length(); + byte[] bytes = new byte[ strlen ]; + byte b1 = 0, b2 = 0, b3 = 0; + + int j = 0; + for ( int i = 0; i < strlen; ++i ) { + int c = string.charAt( i ); + // TODO: combine 2-character combinations + int count; + if ( c > 0x07FF ) { + count = 3; + b3 = (byte)( 0xE0 | ( c >> 12 ) ); + b2 = (byte)( 0x80 | ( ( c >> 6 ) & 0x03F ) ); + b1 = (byte)( 0x80 | ( ( c ) & 0x03F ) ); + } else if ( c > 0x07F ) { + count = 2; + b2 = (byte)( 0xC0 | ( c >> 6 ) ); + b1 = (byte)( 0x80 | ( c & 0x03F ) ); + } else { + count = 1; + b1 = (byte) c; + } + if ( j + count > bytes.length ) { + bytes = realloc( bytes, ( j + count ) * 2 ); + } + switch ( count ) { + case 3: + bytes[j++] = b3; + case 2: + bytes[j++] = b2; + case 1: + bytes[j++] = b1; + } + } + + if ( j != bytes.length ) { + bytes = realloc( bytes, j ); + } + return bytes; + } + + private static byte[] realloc( byte[] a, int newSize ) { + final byte[] newbytes = new byte[ newSize ]; + System.arraycopy( a, 0, newbytes, 0, Math.min( newSize, a.length ) ); + return newbytes; + } + + public int luaByte(int index) { + return m_bytes[m_offset + index] & 0x0FF; + } + +} diff --git a/src/main/java/lua/value/LTable.java b/src/main/java/lua/value/LTable.java new file mode 100644 index 00000000..110041c4 --- /dev/null +++ b/src/main/java/lua/value/LTable.java @@ -0,0 +1,540 @@ +package lua.value; + +import lua.Lua; +import lua.VM; + +/** + * Simple implementation of table structure for Lua VM. Maintains both an array + * part and a hash part. Does not attempt to achieve the same performance as the + * C version. + * + * Java code can put values in the table or get values out (bypassing the + * metatable, if there is one) using put() and get(). There are specializations + * of put() and get() for integers and Strings to avoid allocating wrapper + * objects when possible. + * + * remove() methods are private: setting a key's value to nil is the correct way + * to remove an entry from the table. + * + * TODO: Support for weak tables has to be shoehorned in here somehow. + * + */ +public class LTable extends LValue { + + public static final LString TYPE_NAME = new LString(Type.table.toString()); + + /** + * Zero-length array to use instead of null, so that we don't need to + * check for null everywhere. + */ + private static final LValue[] EMPTY_ARRAY = new LValue[0]; + + /** + * Minimum legal capacity for the hash portion. Note that the hash portion + * must never be filled to capacity or findSlot() will run forever. + */ + private static final int MIN_HASH_CAPACITY = 2; + + /** + * Array of keys in the hash part. When there is no hash part this is null. + * Elements of m_hashKeys are never LNil.NIL - they are null to indicate + * the hash slot is empty and some non-null, non-nil value otherwise. + */ + private LValue[] m_hashKeys; + + /** + * Values in the hash part. Must be null when m_hashKeys is null and equal + * in size otherwise. + */ + private LValue[] m_hashValues; + + /** + * m_hashEntries is the number of slots that are used. Must always be less + * than m_hashKeys.length. + */ + private int m_hashEntries; + + /** + * Array of values to store the "array part" of the table, that is the + * entries with positive integer keys. Elements must never be null: "empty" + * slots are set to LNil.NIL. + */ + private LValue[] m_vector; + + /** + * Number of values in m_vector that non-nil. + */ + private int m_arrayEntries; + + private LTable m_metatable; + + + + /** Construct an empty LTable with no initial capacity. */ + public LTable() { + m_vector = EMPTY_ARRAY; + } + + /** + * Construct an empty LTable that is expected to contain entries with keys + * in the range 1 .. narray and nhash non-integer keys. + */ + public LTable( int narray, int nhash ) { + if ( nhash > 0 ) { + // Allocate arrays 25% bigger than nhash to account for load factor. + final int capacity = Math.max( nhash + ( nhash >> 2 ), nhash + 1 ); + m_hashKeys = new LValue[capacity]; + m_hashValues = new LValue[capacity]; + } + m_vector = new LValue[narray]; + for ( int i = 0; i < narray; ++i ) { + m_vector[i] = LNil.NIL; + } + } + + /** + * Return total number of keys mapped to non-nil values. Not to be confused + * with luaLength, which returns some number n such that the value at n+1 is + * nil. + */ + public int size() { + return m_hashEntries + m_arrayEntries; + } + + /** + * Generic put method for all types of keys, but does not use the metatable. + */ + public void put( LValue key, LValue val ) { + if ( key.isInteger() ) { + // call the integer-specific put method + put( key.luaAsInt(), val ); + } else if ( val == null || val == LNil.NIL ) { + // Remove the key if the value is nil. This comes after the check + // for an integer key so that values are properly removed from + // the array part. + remove( key ); + } else { + if ( checkLoadFactor() ) + rehash(); + int slot = findSlot( key ); + if ( fillHashSlot( slot, val ) ) + return; + m_hashKeys[slot] = key; + } + } + + /** + * Utility method for putting a string-keyed value directly, typically for + * initializing a table. Bypasses the metatable, if any. + */ + public void put( String key, LValue value ) { + put( new LString( key ), value ); + } + + /** + * Method for putting an integer-keyed value. Bypasses the metatable, if + * any. + */ + public void put( int key, LValue value ) { + if (value == null || value == LNil.NIL) { + remove( key ); + return; + } + if ( key > 0 ) { + final int index = key - 1; + for ( ;; ) { + if ( index < m_vector.length ) { + if ( m_vector[index] == LNil.NIL ) { + ++m_arrayEntries; + } + m_vector[index] = value; + return; + } else if ( index < ( m_arrayEntries + 1 ) * 2 ) { + resize( ( m_arrayEntries + 1 ) * 2 ); + } else { + break; + } + } + } + + // No room in array part, use hash part instead. + if ( checkLoadFactor() ) + rehash(); + int slot = findSlot( key ); + if ( fillHashSlot( slot, value ) ) + return; + m_hashKeys[ slot ] = new LInteger( key ); + } + + + /** + * Utility method to directly get the value in a table, without metatable + * calls. Must never return null, use LNil.NIL instead. + */ + public LValue get( LValue key ) { + if ( m_vector.length > 0 && key.isInteger() ) { + final int index = key.luaAsInt() - 1; + if ( index >= 0 && index < m_vector.length ) { + return m_vector[index]; + } + } + + if ( m_hashKeys == null ) + return LNil.NIL; + + int slot = findSlot( key ); + return ( m_hashKeys[slot] != null ) ? m_hashValues[slot] : LNil.NIL; + } + + /** Utility method for retrieving an integer-keyed value */ + public LValue get( int key ) { + if ( key > 0 && key <= m_vector.length ) { + return m_vector[key - 1]; + } + + int slot = findSlot( key ); + return ( m_hashKeys[slot] != null ) ? m_hashValues[slot] : LNil.NIL; + } + + + /** + * Return true if the table contains an entry with the given key, false if + * not. Ignores the metatable. + */ + public boolean containsKey( LValue key ) { + if ( m_vector.length > 0 && key.isInteger() ) { + final int index = key.luaAsInt() - 1; + if ( index >= 0 && index < m_vector.length ) { + final LValue v = m_vector[index]; + return v != LNil.NIL; + } + } + if ( m_hashKeys == null ) + return false; + final int slot = findSlot( key ); + return m_hashKeys[ slot ] != null; + } + + public void luaGetTable(VM vm, LValue table, LValue key) { + LValue v = get(key); + if ( v == LNil.NIL && m_metatable != null ) { + super.luaGetTable( vm, table, key ); + } else { + vm.push(v); + } + } + + public void luaSetTable(VM vm, LValue table, LValue key, LValue val) { + if ( !containsKey( key ) && m_metatable != null ) { + super.luaSetTable( vm, table, key, val ); + } else { + put(key, val); + } + } + + /** + * Return the "length" of this table. This will not return the same result + * as the C version in all cases, but that's ok because the length operation + * on a table where the integer keys are sparse is vaguely defined. + */ + public LValue luaLength() { + for ( int i = Math.max( 0, m_arrayEntries-1 ); i < m_vector.length; ++i ) { + if ( m_vector[i] != LNil.NIL && + ( i+1 == m_vector.length || m_vector[i+1] == LNil.NIL ) ) { + return new LInteger( i+1 ); + } + } + return new LInteger( 0 ); + } + + /** Valid for tables */ + public LTable luaGetMetatable() { + return this.m_metatable; + } + + /** Valid for tables */ + public void luaSetMetatable(LValue metatable) { + this.m_metatable = ( metatable != null && metatable != LNil.NIL ) ? + (LTable) metatable : null; + } + + public LString luaAsString() { + return new LString("table: "+id()); + } + + public LString luaGetType() { + return TYPE_NAME; + } + + /** Valid for tables */ + public LValue luaPairs(boolean isPairs) { + return new LTableIterator(isPairs); + } + + /** Iterator for tables */ + private final class LTableIterator extends LFunction { + private int arrayIndex; + private int hashIndex; + private final boolean isPairs; + + private LTableIterator(boolean isPairs) { + this.arrayIndex = 0; + this.hashIndex = 0; + this.isPairs = isPairs; + } + + // perform a lua call + public boolean luaStackCall(VM vm) { + vm.setResult(); + int i; + while ( ( i = arrayIndex++ ) < m_vector.length ) { + if ( m_vector[i] != LNil.NIL ) { + vm.push( new LInteger( arrayIndex ) ); + vm.push( m_vector[ i ] ); + return false; + } + } + if ( isPairs && (m_hashKeys != null) ) { + while ( ( i = hashIndex++ ) < m_hashKeys.length ) { + if ( m_hashKeys[i] != null ) { + vm.push( m_hashKeys[i] ); + vm.push( m_hashValues[i] ); + return false; + } + } + } + return false; + } + } + + /** + * Helper method to get all the keys in this table in an array. Meant to be + * used instead of keys() (which returns an enumeration) when an array is + * more convenient. Note that for a very large table, getting an Enumeration + * instead would be more space efficient. + */ + public LValue[] getKeys() { + LValue[] keys = new LValue[ m_arrayEntries + m_hashEntries ]; + int out = 0; + + for ( int i = 0; i < m_vector.length; ++i ) { + if ( m_vector[ i ] != LNil.NIL ) { + keys[ out++ ] = new LInteger( i + 1 ); + } + } + + if ( m_hashKeys != null ) { + for ( int i = 0; i < m_hashKeys.length; ++i ) { + if ( m_hashKeys[ i ] != null ) + keys[ out++ ] = m_hashKeys[i]; + } + } + + return keys; + } + + /** Remove the value in the table with the given integer key. */ + private void remove( int key ) { + if ( key > 0 ) { + final int index = key - 1; + if ( index < m_vector.length ) { + if ( m_vector[ index ] != LNil.NIL ) { + --m_arrayEntries; + } + return; + } + } + + if ( m_hashKeys != null ) { + int slot = findSlot( key ); + clearSlot( slot ); + } + } + + private void remove( LValue key ) { + if ( m_hashKeys != null ) { + int slot = findSlot( key ); + clearSlot( slot ); + } + } + + private void clearSlot( int i ) { + if ( m_hashKeys[ i ] != null ) { + + int j = i; + while ( m_hashKeys[ j = ( ( j + 1 ) % m_hashKeys.length ) ] != null ) { + final int k = hashToIndex( m_hashKeys[ j ].hashCode() ); + if ( ( j > i && ( k <= i || k > j ) ) || + ( j < i && ( k <= i && k > j ) ) ) { + m_hashKeys[ i ] = m_hashKeys[ j ]; + m_hashValues[ i ] = m_hashValues[ j ]; + i = j; + } + } + + --m_hashEntries; + m_hashKeys[ i ] = null; + m_hashValues[ i ] = null; + + if ( m_hashEntries == 0 ) { + m_hashKeys = null; + m_hashValues = null; + } + } + } + + private int findSlot( LValue key ) { + int i = hashToIndex( key.hashCode() ); + + // This loop is guaranteed to terminate as long as we never allow the + // table to get 100% full. + LValue k; + while ( ( k = m_hashKeys[i] ) != null && + !key.luaBinCmpUnknown( Lua.OP_EQ, k ) ) { + i = ( i + 1 ) % m_hashKeys.length; + } + return i; + } + + private int findSlot( int key ) { + int i = hashToIndex( LInteger.hashCodeOf( key ) ); + + // This loop is guaranteed to terminate as long as we never allow the + // table to get 100% full. + LValue k; + while ( ( k = m_hashKeys[i] ) != null && + !k.luaBinCmpInteger( Lua.OP_EQ, key ) ) { + i = ( i + 1 ) % m_hashKeys.length; + } + return i; + } + + /** + * @return true if the given slot was already occupied, false otherwise. + */ + private boolean fillHashSlot( int slot, LValue value ) { + m_hashValues[ slot ] = value; + if ( m_hashKeys[ slot ] != null ) { + return true; + } else { + ++m_hashEntries; + return false; + } + } + + private int hashToIndex( int hash ) { + return ( hash & 0x7FFFFFFF ) % m_hashKeys.length; + } + + /** + * Should be called before inserting a value into the hash. + * + * @return true if the hash portion of the LTable is at its capacity. + */ + private boolean checkLoadFactor() { + if ( m_hashKeys == null ) + return true; + // Using a load factor of 2/3 because that is easy to compute without + // overflow or division. + final int hashCapacity = m_hashKeys.length; + return ( hashCapacity >> 1 ) >= ( hashCapacity - m_hashEntries ); + } + + private void rehash() { + final int oldCapacity = ( m_hashKeys != null ) ? m_hashKeys.length : 0; + final int newCapacity = ( oldCapacity > 0 ) ? 2 * oldCapacity : MIN_HASH_CAPACITY; + + final LValue[] oldKeys = m_hashKeys; + final LValue[] oldValues = m_hashValues; + + m_hashKeys = new LValue[ newCapacity ]; + m_hashValues = new LValue[ newCapacity ]; + + for ( int i = 0; i < oldCapacity; ++i ) { + final LValue k = oldKeys[i]; + if ( k != null ) { + final LValue v = oldValues[i]; + final int slot = findSlot( k ); + m_hashKeys[slot] = k; + m_hashValues[slot] = v; + } + } + } + + private void resize( int newCapacity ) { + final int oldCapacity = m_vector.length; + LValue[] newVector = new LValue[ newCapacity ]; + System.arraycopy( m_vector, 0, newVector, 0, Math.min( oldCapacity, newCapacity ) ); + + // We need to move keys from hash part to array part if array part is + // getting bigger, and from array part to hash part if array is getting + // smaller. + if ( newCapacity > oldCapacity ) { + if ( m_hashKeys != null ) { + for ( int i = oldCapacity; i < newCapacity; ++i ) { + int slot = findSlot( i+1 ); + if ( m_hashKeys[ slot ] != null ) { + newVector[ i ] = m_hashValues[ slot ]; + m_hashKeys[ i ] = null; + --m_hashEntries; + } else { + // Make sure all array-part values are initialized to nil + // so that we can just do one compare instead of two + // whenever we need to check if a slot is full or not. + newVector[ i ] = LNil.NIL; + } + } + } else { + for ( int i = oldCapacity; i < newCapacity; ++i ) { + newVector[ i ] = LNil.NIL; + } + } + } else { + for ( int i = newCapacity; i < oldCapacity; ++i ) { + LValue v = m_vector[i]; + if ( v != LNil.NIL ) { + if (checkLoadFactor()) + rehash(); + final int slot = findSlot( i+1 ); + m_hashKeys[ slot ] = new LInteger( i+1 ); + m_hashValues[ slot ] = v; + ++m_hashEntries; + } + } + } + + m_vector = newVector; + } + + // hooks for junit + + int getHashCapacity() { + return ( m_hashKeys != null ) ? m_hashKeys.length : 0; + } + + int getArrayCapacity() { + return m_vector.length; + } + + /* + * @pos index to insert at, or 0 to insert at end. + */ + public void luaInsertPos(int pos, LValue value) { + if ( pos != 0 ) + throw new RuntimeException("luaInsertPos() not implemented"); + put( m_arrayEntries + m_hashEntries + 1, value ); + } + + public void luaSort() { + throw new RuntimeException("luaSort() not implemented"); + } + + public void luaRemovePos(int pos) { + throw new RuntimeException("luaRemovePos() not implemented"); + } + + public int luaMaxN() { + throw new RuntimeException("luaMaxN() not implemented"); + } + +} diff --git a/src/main/java/lua/value/LThread.java b/src/main/java/lua/value/LThread.java new file mode 100644 index 00000000..34a15d1e --- /dev/null +++ b/src/main/java/lua/value/LThread.java @@ -0,0 +1,13 @@ +package lua.value; + +public class LThread extends LValue { + public static final LString TYPE_NAME = new LString(Type.thread.toString()); + + public LString luaGetType() { + return TYPE_NAME; + } + + public LString luaAsString() { + return new LString("thread: "+hashCode()); + } +} diff --git a/src/main/java/lua/value/LUserData.java b/src/main/java/lua/value/LUserData.java new file mode 100644 index 00000000..78c191b9 --- /dev/null +++ b/src/main/java/lua/value/LUserData.java @@ -0,0 +1,33 @@ +package lua.value; + +public class LUserData extends LValue { + public static final LString TYPE_NAME = new LString(Type.userdata.toString()); + + public final Object m_instance; + public LTable m_metatable; + + public LUserData(Object obj) { + m_instance = obj; + } + + public LString luaAsString() { + return new LString(m_instance.toString()); + } + + public boolean equals(Object obj) { + return (this == obj) || + (obj instanceof LUserData && this.m_instance == ((LUserData) obj).m_instance); + } + + public int hashCode() { + return System.identityHashCode( m_instance ); + } + + public LString luaGetType() { + return TYPE_NAME; + } + + public LTable luaGetMetatable() { + return m_metatable; + } +} diff --git a/src/test/java/lua/debug/DebugStackStateTest.java b/src/test/java/lua/debug/DebugStackStateTest.java new file mode 100644 index 00000000..ae17e5fb --- /dev/null +++ b/src/test/java/lua/debug/DebugStackStateTest.java @@ -0,0 +1,56 @@ +package lua.debug; + +import java.io.IOException; +import java.io.InputStream; + +import junit.framework.TestCase; +import lua.io.Closure; +import lua.io.LoadState; +import lua.io.Proto; +import lua.value.LValue; + +public class DebugStackStateTest extends TestCase { + + public void testDebugStackState() throws InterruptedException, IOException { + String script = "/test6.luac"; + + // set up the vm + final DebugStackState state = new DebugStackState(); + InputStream is = getClass().getResourceAsStream( script ); + Proto p = LoadState.undump(state, is, script); + + // create closure and execute + final Closure c = new Closure( state, p ); + + // suspend the vm right away + state.suspend(); + state.setBreakpoint(script, 14); + + // start the call processing in its own thread + new Thread() { + public void run() { + try { + state.doCall( c, new LValue[0] ); + } catch ( Exception e ) { + e.printStackTrace(); + } + } + }.start(); + + // step for 5 steps + for ( int i=0; i<5; i++ ) { + state.step(); + Thread.sleep(500); + System.out.println("--- callgraph="+state.getCallgraph() ); + System.out.println("--- stack="+state.getStack(0) ); + } + + // resume the vm + state.resume(); + Thread.sleep(500); + System.out.println("--- callgraph="+state.getCallgraph() ); + state.resume(); + Thread.sleep(500); + System.out.println("--- callgraph="+state.getCallgraph() ); + } +}