524 lines
18 KiB
Java
524 lines
18 KiB
Java
/*
|
|
* This file is part of ComputerCraft - http://www.computercraft.info
|
|
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
|
|
* Send enquiries to dratcliffe@gmail.com
|
|
*/
|
|
package dan200.computercraft.core.lua;
|
|
|
|
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.tracking.Tracking;
|
|
import dan200.computercraft.core.tracking.TrackingField;
|
|
import dan200.computercraft.shared.util.ThreadUtils;
|
|
import org.squiddev.cobalt.*;
|
|
import org.squiddev.cobalt.compiler.CompileException;
|
|
import org.squiddev.cobalt.compiler.LoadState;
|
|
import org.squiddev.cobalt.debug.DebugFrame;
|
|
import org.squiddev.cobalt.debug.DebugHandler;
|
|
import org.squiddev.cobalt.debug.DebugState;
|
|
import org.squiddev.cobalt.function.LuaFunction;
|
|
import org.squiddev.cobalt.lib.*;
|
|
import org.squiddev.cobalt.lib.platform.VoidResourceManipulator;
|
|
|
|
import javax.annotation.Nonnull;
|
|
import javax.annotation.Nullable;
|
|
import java.io.InputStream;
|
|
import java.nio.ByteBuffer;
|
|
import java.util.*;
|
|
import java.util.concurrent.SynchronousQueue;
|
|
import java.util.concurrent.ThreadPoolExecutor;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
import static org.squiddev.cobalt.ValueFactory.valueOf;
|
|
import static org.squiddev.cobalt.ValueFactory.varargsOf;
|
|
import static org.squiddev.cobalt.debug.DebugFrame.FLAG_HOOKED;
|
|
import static org.squiddev.cobalt.debug.DebugFrame.FLAG_HOOKYIELD;
|
|
|
|
public class CobaltLuaMachine implements ILuaMachine
|
|
{
|
|
private static final ThreadPoolExecutor COROUTINES = new ThreadPoolExecutor(
|
|
0, Integer.MAX_VALUE,
|
|
5L, TimeUnit.MINUTES,
|
|
new SynchronousQueue<>(),
|
|
ThreadUtils.factory( "Coroutine" )
|
|
);
|
|
|
|
private static final LuaMethod FUNCTION_METHOD = ( target, context, args ) -> ((ILuaFunction) target).call( args );
|
|
|
|
private final Computer computer;
|
|
private final TimeoutState timeout;
|
|
private final TimeoutDebugHandler debug;
|
|
private final ILuaContext context;
|
|
|
|
private LuaState state;
|
|
private LuaTable globals;
|
|
|
|
private LuaThread mainRoutine = null;
|
|
private String eventFilter = null;
|
|
|
|
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
|
|
LuaState state = this.state = LuaState.builder()
|
|
.resourceManipulator( new VoidResourceManipulator() )
|
|
.debug( debug )
|
|
.coroutineExecutor( command -> {
|
|
Tracking.addValue( this.computer, TrackingField.COROUTINES_CREATED, 1 );
|
|
COROUTINES.execute( () -> {
|
|
try
|
|
{
|
|
command.run();
|
|
}
|
|
finally
|
|
{
|
|
Tracking.addValue( this.computer, TrackingField.COROUTINES_DISPOSED, 1 );
|
|
}
|
|
} );
|
|
} )
|
|
.build();
|
|
|
|
globals = new LuaTable();
|
|
state.setupThread( globals );
|
|
|
|
// Add basic libraries
|
|
globals.load( state, new BaseLib() );
|
|
globals.load( state, new TableLib() );
|
|
globals.load( state, new StringLib() );
|
|
globals.load( state, new MathLib() );
|
|
globals.load( state, new CoroutineLib() );
|
|
globals.load( state, new Bit32Lib() );
|
|
globals.load( state, new Utf8Lib() );
|
|
if( ComputerCraft.debugEnable ) globals.load( state, new DebugLib() );
|
|
|
|
// Remove globals we don't want to expose
|
|
globals.rawset( "collectgarbage", Constants.NIL );
|
|
globals.rawset( "dofile", Constants.NIL );
|
|
globals.rawset( "loadfile", Constants.NIL );
|
|
globals.rawset( "print", Constants.NIL );
|
|
|
|
// Add version globals
|
|
globals.rawset( "_VERSION", valueOf( "Lua 5.1" ) );
|
|
globals.rawset( "_HOST", valueOf( computer.getAPIEnvironment().getComputerEnvironment().getHostString() ) );
|
|
globals.rawset( "_CC_DEFAULT_SETTINGS", valueOf( ComputerCraft.defaultComputerSettings ) );
|
|
if( ComputerCraft.disableLua51Features )
|
|
{
|
|
globals.rawset( "_CC_DISABLE_LUA51_FEATURES", Constants.TRUE );
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void addAPI( @Nonnull ILuaAPI api )
|
|
{
|
|
// Add the methods of an API to the global table
|
|
LuaTable table = wrapLuaObject( api );
|
|
if( table == null )
|
|
{
|
|
ComputerCraft.log.warn( "API {} does not provide any methods", api );
|
|
table = new LuaTable();
|
|
}
|
|
|
|
String[] names = api.getNames();
|
|
for( String name : names ) globals.rawset( name, table );
|
|
}
|
|
|
|
@Override
|
|
public MachineResult loadBios( @Nonnull InputStream bios )
|
|
{
|
|
// Begin executing a file (ie, the bios)
|
|
if( mainRoutine != null ) return MachineResult.OK;
|
|
|
|
try
|
|
{
|
|
LuaFunction value = LoadState.load( state, bios, "@bios.lua", globals );
|
|
mainRoutine = new LuaThread( state, value, globals );
|
|
return MachineResult.OK;
|
|
}
|
|
catch( CompileException e )
|
|
{
|
|
close();
|
|
return MachineResult.error( e );
|
|
}
|
|
catch( Exception e )
|
|
{
|
|
ComputerCraft.log.warn( "Could not load bios.lua", e );
|
|
close();
|
|
return MachineResult.GENERIC_ERROR;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public MachineResult handleEvent( String eventName, Object[] arguments )
|
|
{
|
|
if( mainRoutine == null ) return MachineResult.OK;
|
|
|
|
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();
|
|
if( !timeout.isSoftAborted() ) debug.thrownSoftAbort = false;
|
|
|
|
try
|
|
{
|
|
Varargs resumeArgs = Constants.NONE;
|
|
if( eventName != null )
|
|
{
|
|
resumeArgs = varargsOf( valueOf( eventName ), toValues( arguments ) );
|
|
}
|
|
|
|
// Resume the current thread, or the main one when first starting off.
|
|
LuaThread thread = state.getCurrentThread();
|
|
if( thread == null || thread == state.getMainThread() ) thread = mainRoutine;
|
|
|
|
Varargs results = LuaThread.run( thread, resumeArgs );
|
|
if( timeout.isHardAborted() ) throw HardAbortError.INSTANCE;
|
|
if( results == null ) return MachineResult.PAUSE;
|
|
|
|
LuaValue filter = results.first();
|
|
eventFilter = filter.isString() ? filter.toString() : null;
|
|
|
|
if( mainRoutine.getStatus().equals( "dead" ) )
|
|
{
|
|
close();
|
|
return MachineResult.GENERIC_ERROR;
|
|
}
|
|
else
|
|
{
|
|
return MachineResult.OK;
|
|
}
|
|
}
|
|
catch( HardAbortError | InterruptedException e )
|
|
{
|
|
close();
|
|
return MachineResult.TIMEOUT;
|
|
}
|
|
catch( LuaError e )
|
|
{
|
|
close();
|
|
ComputerCraft.log.warn( "Top level coroutine errored", e );
|
|
return MachineResult.error( e );
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void close()
|
|
{
|
|
LuaState state = this.state;
|
|
if( state == null ) return;
|
|
|
|
state.abandon();
|
|
mainRoutine = null;
|
|
this.state = null;
|
|
globals = null;
|
|
}
|
|
|
|
@Nullable
|
|
private LuaTable wrapLuaObject( Object object )
|
|
{
|
|
String[] dynamicMethods = object instanceof IDynamicLuaObject
|
|
? Objects.requireNonNull( ((IDynamicLuaObject) object).getMethodNames(), "Methods cannot be null" )
|
|
: LuaMethod.EMPTY_METHODS;
|
|
|
|
LuaTable table = new LuaTable();
|
|
for( int i = 0; i < dynamicMethods.length; i++ )
|
|
{
|
|
String method = dynamicMethods[i];
|
|
table.rawset( method, new ResultInterpreterFunction( this, LuaMethod.DYNAMIC.get( i ), object, context, method ) );
|
|
}
|
|
|
|
ObjectSource.allMethods( LuaMethod.GENERATOR, object, ( instance, method ) ->
|
|
table.rawset( method.getName(), method.nonYielding()
|
|
? new BasicFunction( this, method.getMethod(), instance, context, method.getName() )
|
|
: new ResultInterpreterFunction( this, method.getMethod(), instance, context, method.getName() ) ) );
|
|
|
|
try
|
|
{
|
|
if( table.keyCount() == 0 ) return null;
|
|
}
|
|
catch( LuaError ignored )
|
|
{
|
|
}
|
|
|
|
return table;
|
|
}
|
|
|
|
@Nonnull
|
|
private LuaValue toValue( @Nullable Object object, @Nullable Map<Object, LuaValue> values )
|
|
{
|
|
if( object == null ) return Constants.NIL;
|
|
if( object instanceof Number ) return valueOf( ((Number) object).doubleValue() );
|
|
if( object instanceof Boolean ) return valueOf( (Boolean) object );
|
|
if( object instanceof String ) return valueOf( object.toString() );
|
|
if( object instanceof byte[] )
|
|
{
|
|
byte[] b = (byte[]) object;
|
|
return valueOf( Arrays.copyOf( b, b.length ) );
|
|
}
|
|
if( object instanceof ByteBuffer )
|
|
{
|
|
ByteBuffer b = (ByteBuffer) object;
|
|
byte[] bytes = new byte[b.remaining()];
|
|
b.get( bytes );
|
|
return valueOf( bytes );
|
|
}
|
|
|
|
if( values == null ) values = new IdentityHashMap<>( 1 );
|
|
LuaValue result = values.get( object );
|
|
if( result != null ) return result;
|
|
|
|
if( object instanceof ILuaFunction )
|
|
{
|
|
return new ResultInterpreterFunction( this, FUNCTION_METHOD, object, context, object.toString() );
|
|
}
|
|
|
|
if( object instanceof IDynamicLuaObject )
|
|
{
|
|
LuaValue wrapped = wrapLuaObject( object );
|
|
if( wrapped == null ) wrapped = new LuaTable();
|
|
values.put( object, wrapped );
|
|
return wrapped;
|
|
}
|
|
|
|
if( object instanceof Map )
|
|
{
|
|
LuaTable table = new LuaTable();
|
|
values.put( object, table );
|
|
|
|
for( Map.Entry<?, ?> pair : ((Map<?, ?>) object).entrySet() )
|
|
{
|
|
LuaValue key = toValue( pair.getKey(), values );
|
|
LuaValue value = toValue( pair.getValue(), values );
|
|
if( !key.isNil() && !value.isNil() ) table.rawset( key, value );
|
|
}
|
|
return table;
|
|
}
|
|
|
|
if( object instanceof Collection )
|
|
{
|
|
Collection<?> objects = (Collection<?>) object;
|
|
LuaTable table = new LuaTable( objects.size(), 0 );
|
|
values.put( object, table );
|
|
int i = 0;
|
|
for( Object child : objects ) table.rawset( ++i, toValue( child, values ) );
|
|
return table;
|
|
}
|
|
|
|
if( object instanceof Object[] )
|
|
{
|
|
Object[] objects = (Object[]) object;
|
|
LuaTable table = new LuaTable( objects.length, 0 );
|
|
values.put( object, table );
|
|
for( int i = 0; i < objects.length; i++ ) table.rawset( i + 1, toValue( objects[i], values ) );
|
|
return table;
|
|
}
|
|
|
|
LuaTable wrapped = wrapLuaObject( object );
|
|
if( wrapped != null )
|
|
{
|
|
values.put( object, wrapped );
|
|
return wrapped;
|
|
}
|
|
|
|
if( ComputerCraft.logComputerErrors )
|
|
{
|
|
ComputerCraft.log.warn( "Received unknown type '{}', returning nil.", object.getClass().getName() );
|
|
}
|
|
return Constants.NIL;
|
|
}
|
|
|
|
Varargs toValues( Object[] objects )
|
|
{
|
|
if( objects == null || objects.length == 0 ) return Constants.NONE;
|
|
if( objects.length == 1 ) return toValue( objects[0], null );
|
|
|
|
Map<Object, LuaValue> result = new IdentityHashMap<>( 0 );
|
|
LuaValue[] values = new LuaValue[objects.length];
|
|
for( int i = 0; i < values.length; i++ )
|
|
{
|
|
Object object = objects[i];
|
|
values[i] = toValue( object, result );
|
|
}
|
|
return varargsOf( values );
|
|
}
|
|
|
|
static Object toObject( LuaValue value, Map<LuaValue, Object> objects )
|
|
{
|
|
switch( value.type() )
|
|
{
|
|
case Constants.TNIL:
|
|
case Constants.TNONE:
|
|
return null;
|
|
case Constants.TINT:
|
|
case Constants.TNUMBER:
|
|
return value.toDouble();
|
|
case Constants.TBOOLEAN:
|
|
return value.toBoolean();
|
|
case Constants.TSTRING:
|
|
return value.toString();
|
|
case Constants.TTABLE:
|
|
{
|
|
// Table:
|
|
// Start remembering stuff
|
|
if( objects == null )
|
|
{
|
|
objects = new IdentityHashMap<>( 1 );
|
|
}
|
|
else
|
|
{
|
|
Object existing = objects.get( value );
|
|
if( existing != null ) return existing;
|
|
}
|
|
Map<Object, Object> table = new HashMap<>();
|
|
objects.put( value, table );
|
|
|
|
LuaTable luaTable = (LuaTable) value;
|
|
|
|
// Convert all keys
|
|
LuaValue k = Constants.NIL;
|
|
while( true )
|
|
{
|
|
Varargs keyValue;
|
|
try
|
|
{
|
|
keyValue = luaTable.next( k );
|
|
}
|
|
catch( LuaError luaError )
|
|
{
|
|
break;
|
|
}
|
|
k = keyValue.first();
|
|
if( k.isNil() ) break;
|
|
|
|
LuaValue v = keyValue.arg( 2 );
|
|
Object keyObject = toObject( k, objects );
|
|
Object valueObject = toObject( v, objects );
|
|
if( keyObject != null && valueObject != null )
|
|
{
|
|
table.put( keyObject, valueObject );
|
|
}
|
|
}
|
|
return table;
|
|
}
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static Object[] toObjects( Varargs values )
|
|
{
|
|
int count = values.count();
|
|
Object[] objects = new Object[count];
|
|
for( int i = 0; i < count; i++ ) objects[i] = toObject( values.arg( i + 1 ), null );
|
|
return objects;
|
|
}
|
|
|
|
static IArguments toArguments( Varargs values )
|
|
{
|
|
return values == Constants.NONE ? VarargArguments.EMPTY : new VarargArguments( values );
|
|
}
|
|
|
|
/**
|
|
* A {@link DebugHandler} which observes the {@link TimeoutState} and responds accordingly.
|
|
*/
|
|
private class TimeoutDebugHandler extends DebugHandler
|
|
{
|
|
private final TimeoutState timeout;
|
|
private int count = 0;
|
|
boolean thrownSoftAbort;
|
|
|
|
private boolean isPaused;
|
|
private int oldFlags;
|
|
private boolean oldInHook;
|
|
|
|
TimeoutDebugHandler()
|
|
{
|
|
timeout = CobaltLuaMachine.this.timeout;
|
|
}
|
|
|
|
@Override
|
|
public void onInstruction( DebugState ds, DebugFrame di, int pc ) throws LuaError, UnwindThrowable
|
|
{
|
|
di.pc = pc;
|
|
|
|
if( isPaused ) resetPaused( ds, di );
|
|
|
|
// We check our current pause/abort state every 128 instructions.
|
|
if( (count = (count + 1) & 127) == 0 )
|
|
{
|
|
// If we've been hard aborted or closed then abort.
|
|
if( timeout.isHardAborted() || state == null ) throw HardAbortError.INSTANCE;
|
|
|
|
timeout.refresh();
|
|
if( timeout.isPaused() )
|
|
{
|
|
// Preserve the current state
|
|
isPaused = true;
|
|
oldInHook = ds.inhook;
|
|
oldFlags = di.flags;
|
|
|
|
// Suspend the state. This will probably throw, but we need to handle the case where it won't.
|
|
di.flags |= FLAG_HOOKYIELD | FLAG_HOOKED;
|
|
LuaThread.suspend( ds.getLuaState() );
|
|
resetPaused( ds, di );
|
|
}
|
|
|
|
handleSoftAbort();
|
|
}
|
|
|
|
super.onInstruction( ds, di, pc );
|
|
}
|
|
|
|
@Override
|
|
public void poll() throws LuaError
|
|
{
|
|
// If we've been hard aborted or closed then abort.
|
|
LuaState state = CobaltLuaMachine.this.state;
|
|
if( timeout.isHardAborted() || state == null ) throw HardAbortError.INSTANCE;
|
|
|
|
timeout.refresh();
|
|
if( timeout.isPaused() ) LuaThread.suspendBlocking( state );
|
|
handleSoftAbort();
|
|
}
|
|
|
|
private void resetPaused( DebugState ds, DebugFrame di )
|
|
{
|
|
// Restore the previous paused state
|
|
isPaused = false;
|
|
ds.inhook = oldInHook;
|
|
di.flags = oldFlags;
|
|
}
|
|
|
|
private void handleSoftAbort() throws LuaError
|
|
{
|
|
// If we already thrown our soft abort error then don't do it again.
|
|
if( !timeout.isSoftAborted() || thrownSoftAbort ) return;
|
|
|
|
thrownSoftAbort = true;
|
|
throw new LuaError( TimeoutState.ABORT_MESSAGE );
|
|
}
|
|
}
|
|
|
|
private static final class HardAbortError extends Error
|
|
{
|
|
private static final long serialVersionUID = 7954092008586367501L;
|
|
|
|
static final HardAbortError INSTANCE = new HardAbortError();
|
|
|
|
private HardAbortError()
|
|
{
|
|
super( "Hard Abort", null, true, false );
|
|
}
|
|
}
|
|
}
|