[skip ci] Waluaigi POC

See 12ef20d0fc/README.md
for the performance numbers, which are pretty untenable.
This commit is contained in:
Jonathan Coates 2021-05-03 17:54:11 +01:00
parent 53dd15a213
commit 7e74725de7
9 changed files with 646 additions and 52 deletions

View File

@ -104,6 +104,7 @@ accessTransformer file('src/main/resources/META-INF/accesstransformer.cfg')
}
repositories {
mavenLocal()
mavenCentral()
maven {
name "SquidDev"
@ -130,6 +131,7 @@ accessTransformer file('src/main/resources/META-INF/accesstransformer.cfg')
compileOnly 'com.google.auto.service:auto-service:1.0-rc7'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7'
shade 'cc.tweaked.waluaigi:waluaigi:1.0-SNAPSHOT'
shade 'org.squiddev:Cobalt:0.5.1-SNAPSHOT'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'

View File

@ -13,9 +13,9 @@
import dan200.computercraft.core.apis.*;
import dan200.computercraft.core.filesystem.FileSystem;
import dan200.computercraft.core.filesystem.FileSystemException;
import dan200.computercraft.core.lua.CobaltLuaMachine;
import dan200.computercraft.core.lua.ILuaMachine;
import dan200.computercraft.core.lua.MachineResult;
import dan200.computercraft.core.lua.waluaigi.WaluaigiLuaMachine;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.tracking.Tracking;
import dan200.computercraft.shared.util.Colour;
@ -386,7 +386,7 @@ private ILuaMachine createLuaMachine()
}
// Create the lua machine
ILuaMachine machine = new CobaltLuaMachine( computer, timeout );
ILuaMachine machine = new WaluaigiLuaMachine( computer, timeout );
// Add the APIs. We unwrap them (yes, this is horrible) to get access to the underlying object.
for( ILuaAPI api : apis ) machine.addAPI( api instanceof ApiWrapper ? ((ApiWrapper) api).getDelegate() : api );

View File

@ -10,7 +10,6 @@
import dan200.computercraft.core.asm.LuaMethod;
import dan200.computercraft.core.asm.ObjectSource;
import dan200.computercraft.core.computer.Computer;
import dan200.computercraft.core.computer.MainThread;
import dan200.computercraft.core.computer.TimeoutState;
import dan200.computercraft.core.tracking.Tracking;
import dan200.computercraft.core.tracking.TrackingField;
@ -53,7 +52,7 @@ public class CobaltLuaMachine implements ILuaMachine
private final Computer computer;
private final TimeoutState timeout;
private final TimeoutDebugHandler debug;
private final ILuaContext context = new CobaltLuaContext();
private final ILuaContext context;
private LuaState state;
private LuaTable globals;
@ -65,6 +64,7 @@ public CobaltLuaMachine( Computer computer, TimeoutState timeout )
{
this.computer = computer;
this.timeout = timeout;
this.context = new LuaContext( computer );
debug = new TimeoutDebugHandler();
// Create an environment to run in
@ -509,53 +509,6 @@ private void handleSoftAbort() throws LuaError
}
}
private class CobaltLuaContext implements ILuaContext
{
@Override
public long issueMainThreadTask( @Nonnull final ILuaTask task ) throws LuaException
{
// Issue command
final long taskID = MainThread.getUniqueTaskID();
final Runnable iTask = () -> {
try
{
Object[] results = task.execute();
if( results != null )
{
Object[] eventArguments = new Object[results.length + 2];
eventArguments[0] = taskID;
eventArguments[1] = true;
System.arraycopy( results, 0, eventArguments, 2, results.length );
computer.queueEvent( "task_complete", eventArguments );
}
else
{
computer.queueEvent( "task_complete", new Object[] { taskID, true } );
}
}
catch( LuaException e )
{
computer.queueEvent( "task_complete", new Object[] { taskID, false, e.getMessage() } );
}
catch( Throwable t )
{
if( ComputerCraft.logComputerErrors ) ComputerCraft.log.error( "Error running task", t );
computer.queueEvent( "task_complete", new Object[] {
taskID, false, "Java Exception Thrown: " + t,
} );
}
};
if( computer.queueMainThread( iTask ) )
{
return taskID;
}
else
{
throw new LuaException( "Task limit exceeded" );
}
}
}
private static final class HardAbortError extends Error
{
private static final long serialVersionUID = 7954092008586367501L;

View File

@ -0,0 +1,64 @@
package dan200.computercraft.core.lua;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.api.lua.ILuaContext;
import dan200.computercraft.api.lua.ILuaTask;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.core.computer.Computer;
import dan200.computercraft.core.computer.MainThread;
import javax.annotation.Nonnull;
public class LuaContext implements ILuaContext
{
private final Computer computer;
public LuaContext( Computer computer )
{
this.computer = computer;
}
@Override
public long issueMainThreadTask( @Nonnull final ILuaTask task ) throws LuaException
{
// Issue command
final long taskID = MainThread.getUniqueTaskID();
final Runnable iTask = () -> {
try
{
Object[] results = task.execute();
if( results != null )
{
Object[] eventArguments = new Object[results.length + 2];
eventArguments[0] = taskID;
eventArguments[1] = true;
System.arraycopy( results, 0, eventArguments, 2, results.length );
computer.queueEvent( "task_complete", eventArguments );
}
else
{
computer.queueEvent( "task_complete", new Object[] { taskID, true } );
}
}
catch( LuaException e )
{
computer.queueEvent( "task_complete", new Object[] { taskID, false, e.getMessage() } );
}
catch( Throwable t )
{
if( ComputerCraft.logComputerErrors ) ComputerCraft.log.error( "Error running task", t );
computer.queueEvent( "task_complete", new Object[] {
taskID, false, "Java Exception Thrown: " + t,
} );
}
};
if( computer.queueMainThread( iTask ) )
{
return taskID;
}
else
{
throw new LuaException( "Task limit exceeded" );
}
}
}

View File

@ -0,0 +1,457 @@
package dan200.computercraft.core.lua.waluaigi;
import cc.tweaked.waluaigi.*;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.api.lua.*;
import dan200.computercraft.core.asm.LuaMethod;
import dan200.computercraft.core.asm.ObjectSource;
import dan200.computercraft.core.computer.Computer;
import dan200.computercraft.core.computer.TimeoutState;
import dan200.computercraft.core.lua.LuaContext;
import dan200.computercraft.core.lua.MachineResult;
import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue;
import it.unimi.dsi.fastutil.ints.IntPriorityQueue;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
class LuaState implements ExecutionEnvironment
{
private static final int INITIAL_MEMORY = 1 * 1024 * 1024;
private static final int STRING_BUFFER_SIZE = 1024;
private static final IDynamicLuaObject[] EMPTY_OBJECTS = new IDynamicLuaObject[0];
private final ILuaContext context;
private final Lua lua;
private final Memory memory;
private final TimeoutState timeout;
private int insnCount = 0;
private IDynamicLuaObject[] objects = EMPTY_OBJECTS;
private final IntPriorityQueue freeSlots = new IntArrayFIFOQueue( 0 );
private int nextSlot = 0;
private final int mainState;
private final int childState;
private final int intBox;
private final int stringBuffer;
private String eventFilter;
public LuaState( int maxMemory, Computer computer, TimeoutState timeout )
{
this.context = new LuaContext( computer );
this.timeout = timeout;
this.memory = new Memory( INITIAL_MEMORY, maxMemory );
this.lua = LuaFactory.create( this, memory );
this.intBox = lua.malloc( 4 );
this.stringBuffer = lua.malloc( STRING_BUFFER_SIZE );
this.mainState = lua.waluaigi_new_state();
this.childState = lua.lua_newthread( mainState );
}
public boolean isOpen()
{
return mainState != 0 && childState != 0 && intBox != 0 && stringBuffer != 0;
}
public int allocate( IDynamicLuaObject object )
{
if( !freeSlots.isEmpty() )
{
int slot = freeSlots.dequeueInt();
objects[slot] = object;
return slot;
}
if( nextSlot >= objects.length )
{
objects = objects == EMPTY_OBJECTS ? new IDynamicLuaObject[16] : Arrays.copyOf( objects, objects.length * 2 );
}
int slot = nextSlot++;
objects[slot] = object;
return slot;
}
@Override
public void free( int object )
{
if( object < 0 || object >= objects.length || objects[object] == null ) return;
objects[object] = null;
freeSlots.enqueue( object );
}
@Override
public int invoke( int luaState, int object, int method )
{
IDynamicLuaObject luaObject;
if( object < 0 || object >= objects.length || (luaObject = objects[object]) == null )
{
pushString( luaState, "Invalid object" );
lua.lua_error( luaState );
throw new UnreachableException();
}
int nargs = lua.lua_gettop( luaState );
Object[] args = new Object[nargs];
for( int i = 0; i < nargs; i++ ) args[i] = fromLua( luaState, i + 1 );
MethodResult result;
try
{
result = luaObject.callMethod( context, method, new ObjectArguments( args ) );
}
catch( LuaException e )
{
lua.luaL_where( luaState, e.getLevel() );
pushString( luaState, e.getMessage() );
lua.lua_concat( luaState, 2 );
lua.lua_error( luaState );
throw new UnreachableException();
}
if( result.getCallback() != null ) throw new IllegalStateException( "NYI: Yields" );
Object[] values = result.getResult();
if( values == null ) return 0;
for( Object value : values ) pushObject( luaState, value );
return values.length;
}
private Object fromLua( int luaState, int slot )
{
int type = lua.lua_type( luaState, slot );
switch( type )
{
case LuaConstants.LUA_TNIL:
return null;
case LuaConstants.LUA_TBOOLEAN:
return lua.lua_toboolean( luaState, slot ) != 0;
case LuaConstants.LUA_TLIGHTUSERDATA:
return null;
case LuaConstants.LUA_TNUMBER:
return lua.lua_tonumberx( luaState, slot, 0 );
case LuaConstants.LUA_TSTRING:
return getString( luaState, slot );
case LuaConstants.LUA_TTABLE:
ComputerCraft.log.warn( "NYI: conversion from table" );
// TODO: Conversion fom tables
return null;
case LuaConstants.LUA_TFUNCTION:
case LuaConstants.LUA_TUSERDATA:
case LuaConstants.LUA_TTHREAD:
default:
return null;
}
}
public void addAPI( ILuaAPI api )
{
int mainState = childState;
pushLuaObject( mainState, api, true );
for( String name : api.getNames() )
{
lua.lua_pushvalue( mainState, -1 );
setShortString( name ); // TODO: Support long strings.
lua.lua_setglobal( mainState, stringBuffer );
}
lua.lua_settop( mainState, -2 );
}
public MachineResult handleEvent( @Nullable String eventName, @Nullable Object[] arguments )
{
ComputerCraft.log.warn( "Resuming computer {} vs {}", eventName, eventFilter );
if( eventFilter != null && eventName != null && !eventName.equals( eventFilter ) && !eventName.equals( "terminate" ) )
{
return MachineResult.OK;
}
// If the soft abort has been cleared then we can reset our flag.
timeout.refresh();
int args = 0;
if( eventName != null )
{
args++;
pushString( childState, eventName );
}
if( arguments != null )
{
args += arguments.length;
for( Object object : arguments ) pushObject( childState, object );
}
int result = lua.lua_resume( childState, 0, args, intBox );
switch( result )
{
case LuaConstants.LUA_OK:
{
ComputerCraft.log.warn( "Coroutine finished for no reason!" );
return MachineResult.GENERIC_ERROR;
}
case LuaConstants.LUA_YIELD:
{
int nargs = memory.getInt( intBox );
eventFilter = nargs > 0 && lua.lua_type( childState, 1 ) == LuaConstants.LUA_TSTRING
? getString( childState, 1 )
: null;
lua.lua_settop( childState, -nargs - 1 );
ComputerCraft.log.warn( "Computer yielded OK with {}", eventFilter );
return MachineResult.OK;
}
default:
{
String error = getString( childState, -1 );
ComputerCraft.log.warn( "Top level coroutine errored ({})", error );
return MachineResult.error( error );
}
}
}
private boolean pushDynamicObject( int luaState, IDynamicLuaObject object )
{
lua.waluaigi_pushobject( luaState, allocate( object ) );
String[] methods = Objects.requireNonNull( object.getMethodNames(), "Methods cannot be null" );
for( int i = 0; i < methods.length; i++ )
{
pushString( luaState, methods[i] );
lua.lua_pushvalue( luaState, -2 );
lua.waluaigi_pushfunction( luaState, i );
lua.lua_rawset( luaState, -4 );
}
lua.lua_settop( luaState, -2 );
return methods.length > 0;
}
private void pushLuaObject( int luaState, Object object, boolean force )
{
boolean hasMethods = false;
lua.lua_createtable( luaState, 0, 0 );
if( object instanceof IDynamicLuaObject )
{
hasMethods = pushDynamicObject( luaState, (IDynamicLuaObject) object );
}
hasMethods |= pushDynamicObject( luaState, new WrappedLuaObject( object, LuaMethod.GENERATOR.getMethods( object.getClass() ) ) );
if( object instanceof ObjectSource )
{
for( Object extra : ((ObjectSource) object).getExtra() )
{
hasMethods |= pushDynamicObject( luaState, new WrappedLuaObject( extra, LuaMethod.GENERATOR.getMethods( extra.getClass() ) ) );
}
}
if( hasMethods ) return;
ComputerCraft.log.warn( "Received unknown type '{}'.", object.getClass().getName() );
if( !force )
{
lua.lua_settop( luaState, -2 );
lua.lua_pushnil( luaState );
}
}
private void pushObject( int luaState, Object object )
{
if( object == null )
{
lua.lua_pushnil( luaState );
}
else if( object instanceof Long || object instanceof Integer )
{
lua.lua_pushinteger( luaState, ((Number) object).longValue() );
}
else if( object instanceof Number )
{
lua.lua_pushnumber( luaState, ((Number) object).doubleValue() );
}
else if( object instanceof Boolean )
{
lua.lua_pushboolean( luaState, (Boolean) object ? 1 : 0 );
}
else if( object instanceof String )
{
pushString( luaState, (String) object );
}
else if( object instanceof byte[] )
{
byte[] b = (byte[]) object;
pushString( luaState, b, 0, b.length );
}
else if( object instanceof ByteBuffer )
{
pushString( luaState, (ByteBuffer) object );
}
else if( object instanceof IDynamicLuaObject )
{
pushLuaObject( luaState, object, true );
}
else if( object instanceof Map )
{
Map<?, ?> map = (Map<?, ?>) object;
lua.lua_createtable( luaState, 0, map.size() );
for( Map.Entry<?, ?> pair : map.entrySet() )
{
pushObject( luaState, pair.getKey() );
pushObject( luaState, pair.getValue() );
lua.lua_rawset( luaState, -3 );
}
}
else if( object instanceof Collection )
{
Collection<?> collection = (Collection<?>) object;
lua.lua_createtable( luaState, collection.size(), 0 );
int i = 0;
for( Object obj : collection )
{
pushObject( luaState, obj );
lua.lua_rawseti( luaState, -2, ++i );
}
}
else if( object instanceof Object[] )
{
Object[] collection = (Object[]) object;
lua.lua_createtable( luaState, collection.length, 0 );
for( int i = 0; i < collection.length; i++ )
{
pushObject( luaState, collection[i] );
lua.lua_rawseti( luaState, -2, i + 1 );
}
}
else
{
pushLuaObject( luaState, object, false );
}
}
@Override
public TimeoutMode getTimeout()
{
if( timeout.isHardAborted() ) return TimeoutMode.HALT;
if( (insnCount = (insnCount + 1) & 127) == 0 )
{
timeout.refresh();
if( timeout.isSoftAborted() ) return TimeoutMode.ERROR;
if( timeout.isPaused() ) return TimeoutMode.PAUSE;
}
return TimeoutMode.OK;
}
public MachineResult loadString( ByteBuffer contents, String name )
{
int contentsLen = contents.remaining();
int contentsLoc = lua.malloc( contentsLen );
memory.put( contentsLoc, contents );
setShortString( name );
int result = lua.luaL_loadbufferx( childState, contentsLoc, contentsLen, stringBuffer, 0 );
lua.free( contentsLoc );
return result == 0 ? MachineResult.OK : MachineResult.error( "Cannot parse bios.lua: " + getString( childState, 1 ) );
}
private void setShortString( String string )
{
int length = string.length();
if( length >= STRING_BUFFER_SIZE ) throw new IllegalArgumentException( "String must be <1024 characters" );
memory.putString( stringBuffer, string );
memory.put( stringBuffer + string.length(), (byte) 0 );
}
public void pushString( int luaState, String string )
{
int length = string.length();
if( length <= STRING_BUFFER_SIZE )
{
memory.putString( stringBuffer, string );
lua.lua_pushlstring( luaState, stringBuffer, length );
}
else
{
int position = lua.malloc( length + 1 );
if( position == 0 ) throw new OutOfMemoryException( "Cannot allocate string" );
memory.putString( position, string );
lua.lua_pushlstring( luaState, position, length );
lua.free( position );
}
}
public void pushString( int luaState, ByteBuffer buffer )
{
int length = buffer.remaining();
if( length <= STRING_BUFFER_SIZE )
{
memory.put( stringBuffer, buffer );
lua.lua_pushlstring( luaState, stringBuffer, length );
}
else
{
int position = lua.malloc( length );
if( position == 0 ) throw new OutOfMemoryException( "Cannot allocate string" );
memory.put( position, buffer );
lua.lua_pushlstring( luaState, position, length );
lua.free( position );
}
}
public void pushString( int luaState, byte[] bytes, int offset, int length )
{
if( length <= STRING_BUFFER_SIZE )
{
memory.put( stringBuffer, bytes, offset, length );
lua.lua_pushlstring( luaState, stringBuffer, length );
}
else
{
int position = lua.malloc( length );
if( position == 0 ) throw new OutOfMemoryException( "Cannot allocate string" );
memory.put( position, bytes, offset, length );
lua.lua_pushlstring( luaState, position, length );
lua.free( position );
}
}
public String getString( int luaState, int slot )
{
int stringPos = lua.luaL_checklstring( luaState, slot, intBox );
if( stringPos == 0 ) throw new NullPointerException( "luaL_checklstring returned 0" );
return memory.getString( stringPos, memory.getInt( intBox ) );
}
public ByteBuffer getStringAsBuffer( int luaState, int slot )
{
int stringPos = lua.luaL_checklstring( luaState, slot, intBox );
if( stringPos == 0 ) throw new NullPointerException( "luaL_checklstring returned 0" );
int length = memory.getInt( intBox );
ByteBuffer buffer = ByteBuffer.allocate( length );
memory.get( stringPos, buffer );
buffer.flip();
return buffer;
}
}

View File

@ -0,0 +1,9 @@
package dan200.computercraft.core.lua.waluaigi;
class UnreachableException extends IllegalStateException
{
public UnreachableException()
{
super( "Unreachable code" );
}
}

View File

@ -0,0 +1,70 @@
package dan200.computercraft.core.lua.waluaigi;
import com.google.common.io.ByteStreams;
import dan200.computercraft.ComputerCraft;
import dan200.computercraft.api.lua.ILuaAPI;
import dan200.computercraft.core.computer.Computer;
import dan200.computercraft.core.computer.TimeoutState;
import dan200.computercraft.core.lua.ILuaMachine;
import dan200.computercraft.core.lua.MachineResult;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
public class WaluaigiLuaMachine implements ILuaMachine
{
private final Computer computer;
private final TimeoutState timeout;
private LuaState state;
public WaluaigiLuaMachine( Computer computer, TimeoutState timeout )
{
this.computer = computer;
this.timeout = timeout;
this.state = new LuaState( 128 * 1024 * 1024, computer, timeout );
}
@Override
public void addAPI( @Nonnull ILuaAPI api )
{
if( state != null && state.isOpen() ) state.addAPI( api );
}
@Override
public MachineResult loadBios( @Nonnull InputStream bios )
{
if( state == null || !state.isOpen() ) return MachineResult.error( "Could not create Lua state" );
byte[] bytes;
try
{
bytes = ByteStreams.toByteArray( bios );
}
catch( IOException e )
{
ComputerCraft.log.error( "Error reading bios", e );
return MachineResult.error( "Could not load bios" );
}
MachineResult load = state.loadString( ByteBuffer.wrap( bytes ), "@bios.lua" );
if( load.isError() ) return load;
return MachineResult.OK;
}
@Override
public MachineResult handleEvent( @Nullable String eventName, @Nullable Object[] arguments )
{
if( state == null || !state.isOpen() ) return MachineResult.error( "Machine is not open" );
return state.handleEvent( eventName, arguments );
}
@Override
public void close()
{
state = null;
}
}

View File

@ -0,0 +1,39 @@
package dan200.computercraft.core.lua.waluaigi;
import dan200.computercraft.api.lua.*;
import dan200.computercraft.core.asm.LuaMethod;
import dan200.computercraft.core.asm.NamedMethod;
import javax.annotation.Nonnull;
import java.util.List;
class WrappedLuaObject implements IDynamicLuaObject
{
private final Object object;
private final List<NamedMethod<LuaMethod>> methods;
public WrappedLuaObject( Object object, List<NamedMethod<LuaMethod>> methods )
{
this.object = object;
this.methods = methods;
}
@Nonnull
@Override
public String[] getMethodNames()
{
// TODO: Make this less allocation happy.
String[] methods = new String[this.methods.size()];
int i = 0;
for( NamedMethod<LuaMethod> method : this.methods ) methods[i++] = method.getName();
return methods;
}
@Nonnull
@Override
public MethodResult callMethod( @Nonnull ILuaContext context, int method, @Nonnull IArguments arguments ) throws LuaException
{
if( method < 0 || method >= methods.size() ) throw new LuaException( "Unknown method" );
return methods.get( method ).getMethod().apply( object, context, arguments );
}
}

View File

@ -7,7 +7,7 @@ local expect
do
local h = fs.open("rom/modules/main/cc/expect.lua", "r")
local f, err = loadstring(h.readAll(), "@expect.lua")
local f, err = load(h.readAll(), "@expect.lua")
h.close()
if not f then error(err) end