From 3eab2a9b57968525296b93e0228711866267a88f Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Tue, 7 Dec 2021 18:27:29 +0000 Subject: [PATCH] Add support for a zero-copy Lua table The API is entirely designed for the needs of the speaker right now, so doesn't do much else. --- .../computercraft/api/lua/IArguments.java | 48 ++++++ .../computercraft/api/lua/LuaFunction.java | 11 +- .../computercraft/api/lua/LuaTable.java | 114 ++++++++++++++ .../computercraft/api/lua/LuaValues.java | 28 ++++ .../api/lua/ObjectArguments.java | 34 +++++ .../computercraft/api/lua/ObjectLuaTable.java | 73 +++++++++ .../computercraft/core/asm/Generator.java | 25 ++-- .../computercraft/core/asm/Reflect.java | 4 +- .../computercraft/core/lua/BasicFunction.java | 4 + .../core/lua/CobaltLuaMachine.java | 1 + .../core/lua/ResultInterpreterFunction.java | 4 + .../computercraft/core/lua/TableImpl.java | 141 ++++++++++++++++++ .../core/lua/VarargArguments.java | 36 +++++ .../core/ComputerTestDelegate.java | 2 +- .../computercraft/core/asm/GeneratorTest.java | 22 +++ 15 files changed, 535 insertions(+), 12 deletions(-) create mode 100644 src/main/java/dan200/computercraft/api/lua/LuaTable.java create mode 100644 src/main/java/dan200/computercraft/api/lua/ObjectLuaTable.java create mode 100644 src/main/java/dan200/computercraft/core/lua/TableImpl.java diff --git a/src/main/java/dan200/computercraft/api/lua/IArguments.java b/src/main/java/dan200/computercraft/api/lua/IArguments.java index 02e8b659d..6bd53ea7d 100644 --- a/src/main/java/dan200/computercraft/api/lua/IArguments.java +++ b/src/main/java/dan200/computercraft/api/lua/IArguments.java @@ -183,6 +183,24 @@ public interface IArguments return (Map) value; } + /** + * Get an argument as a table in an unsafe manner. + * + * Classes implementing this interface may choose to implement a more optimised version which does not copy the + * table, instead returning a wrapper version, making it more efficient. However, the caller must guarantee that + * they do not access off the computer thread (and so should not be used with main-thread functions) or once the + * function call has finished (for instance, in callbacks). + * + * @param index The argument number. + * @return The argument's value. + * @throws LuaException If the value is not a table. + */ + @Nonnull + default LuaTable getTableUnsafe( int index ) throws LuaException + { + return new ObjectLuaTable( getTable( index ) ); + } + /** * Get an argument as a double. * @@ -314,6 +332,27 @@ public interface IArguments return Optional.of( (Map) value ); } + /** + * Get an argument as a table in an unsafe manner. + * + * Classes implementing this interface may choose to implement a more optimised version which does not copy the + * table, instead returning a wrapper version, making it more efficient. However, the caller must guarantee that + * they do not access off the computer thread (and so should not be used with main-thread functions) or once the + * function call has finished (for instance, in callbacks). + * + * @param index The argument number. + * @return The argument's value, or {@link Optional#empty()} if not present. + * @throws LuaException If the value is not a table. + */ + @Nonnull + default Optional> optTableUnsafe( int index ) throws LuaException + { + Object value = get( index ); + if( value == null ) return Optional.empty(); + if( !(value instanceof Map) ) throw LuaValues.badArgumentOf( index, "map", value ); + return Optional.of( new ObjectLuaTable( (Map) value ) ); + } + /** * Get an argument as a double. * @@ -404,4 +443,13 @@ public interface IArguments { return optTable( index ).orElse( def ); } + + /** + * This is called when the current function finishes, before any main thread tasks have run. + * + * Called when the current function returns, and so some values are no longer guaranteed to be safe to access. + */ + default void releaseImmediate() + { + } } diff --git a/src/main/java/dan200/computercraft/api/lua/LuaFunction.java b/src/main/java/dan200/computercraft/api/lua/LuaFunction.java index 5558b153c..0e14db58d 100644 --- a/src/main/java/dan200/computercraft/api/lua/LuaFunction.java +++ b/src/main/java/dan200/computercraft/api/lua/LuaFunction.java @@ -51,8 +51,17 @@ public @interface LuaFunction * Run this function on the main server thread. This should be specified for any method which interacts with * Minecraft in a thread-unsafe manner. * - * @return Whether this functi + * @return Whether this function should be run on the main thread. * @see ILuaContext#issueMainThreadTask(ILuaTask) */ boolean mainThread() default false; + + /** + * Allow using "unsafe" arguments, such {@link IArguments#getTableUnsafe(int)}. + * + * This is incompatible with {@link #mainThread()}. + * + * @return Whether this function supports unsafe arguments. + */ + boolean unsafe() default false; } diff --git a/src/main/java/dan200/computercraft/api/lua/LuaTable.java b/src/main/java/dan200/computercraft/api/lua/LuaTable.java new file mode 100644 index 000000000..6bbafe6fd --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/LuaTable.java @@ -0,0 +1,114 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ +package dan200.computercraft.api.lua; + +import org.jetbrains.annotations.Nullable; + +import javax.annotation.Nonnull; +import java.util.Map; + +import static dan200.computercraft.api.lua.LuaValues.*; + +public interface LuaTable extends Map +{ + /** + * Compute the length of the array part of this table. + * + * @return This table's length. + */ + default int length() + { + int size = 0; + while( containsKey( (double) (size + 1) ) ) size++; + return size; + } + + /** + * Get an array entry as an integer. + * + * @param index The index in the table, starting at 1. + * @return The table's value. + * @throws LuaException If the value is not an integer. + */ + default long getLong( int index ) throws LuaException + { + Object value = get( (double) index ); + if( !(value instanceof Number) ) throw badTableItem( index, "number", getType( value ) ); + + Number number = (Number) value; + double asDouble = number.doubleValue(); + if( !Double.isFinite( asDouble ) ) throw badTableItem( index, "number", getNumericType( asDouble ) ); + return number.longValue(); + } + + /** + * Get a table entry as an integer. + * + * @param key The name of the field in the table. + * @return The table's value. + * @throws LuaException If the value is not an integer. + */ + default long getLong( String key ) throws LuaException + { + Object value = get( key ); + if( !(value instanceof Number) ) throw badField( key, "number", getType( value ) ); + + Number number = (Number) value; + double asDouble = number.doubleValue(); + if( !Double.isFinite( asDouble ) ) throw badField( key, "number", getNumericType( asDouble ) ); + return number.longValue(); + } + + /** + * Get an array entry as an integer. + * + * @param index The index in the table, starting at 1. + * @return The table's value. + * @throws LuaException If the value is not an integer. + */ + default int getInt( int index ) throws LuaException + { + return (int) getLong( index ); + } + + /** + * Get a table entry as an integer. + * + * @param key The name of the field in the table. + * @return The table's value. + * @throws LuaException If the value is not an integer. + */ + default int getInt( String key ) throws LuaException + { + return (int) getLong( key ); + } + + + @Nullable + @Override + default V put( K o, V o2 ) + { + throw new UnsupportedOperationException( "Cannot modify LuaTable" ); + } + + @Override + default V remove( Object o ) + { + throw new UnsupportedOperationException( "Cannot modify LuaTable" ); + } + + @Override + default void putAll( @Nonnull Map map ) + { + throw new UnsupportedOperationException( "Cannot modify LuaTable" ); + } + + @Override + default void clear() + { + throw new UnsupportedOperationException( "Cannot modify LuaTable" ); + } +} diff --git a/src/main/java/dan200/computercraft/api/lua/LuaValues.java b/src/main/java/dan200/computercraft/api/lua/LuaValues.java index 8eec784ff..bc984036e 100644 --- a/src/main/java/dan200/computercraft/api/lua/LuaValues.java +++ b/src/main/java/dan200/computercraft/api/lua/LuaValues.java @@ -102,6 +102,34 @@ public final class LuaValues return new LuaException( "bad argument #" + (index + 1) + " (" + expected + " expected, got " + actual + ")" ); } + /** + * Construct a table item exception, from an expected and actual type. + * + * @param index The index into the table, starting from 1. + * @param expected The expected type for this table item. + * @param actual The provided type for this table item. + * @return The constructed exception, which should be thrown immediately. + */ + @Nonnull + public static LuaException badTableItem( int index, @Nonnull String expected, @Nonnull String actual ) + { + return new LuaException( "table item #" + index + " is not " + expected + " (got " + actual + ")" ); + } + + /** + * Construct a field exception, from an expected and actual type. + * + * @param key The name of the field. + * @param expected The expected type for this table item. + * @param actual The provided type for this table item. + * @return The constructed exception, which should be thrown immediately. + */ + @Nonnull + public static LuaException badField( String key, @Nonnull String expected, @Nonnull String actual ) + { + return new LuaException( "field " + key + " is not " + expected + " (got " + actual + ")" ); + } + /** * Ensure a numeric argument is finite (i.e. not infinite or {@link Double#NaN}. * diff --git a/src/main/java/dan200/computercraft/api/lua/ObjectArguments.java b/src/main/java/dan200/computercraft/api/lua/ObjectArguments.java index 6dd7e3c0f..710fc1e34 100644 --- a/src/main/java/dan200/computercraft/api/lua/ObjectArguments.java +++ b/src/main/java/dan200/computercraft/api/lua/ObjectArguments.java @@ -5,10 +5,12 @@ */ package dan200.computercraft.api.lua; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.Optional; /** * An implementation of {@link IArguments} which wraps an array of {@link Object}. @@ -16,6 +18,8 @@ import java.util.Objects; public final class ObjectArguments implements IArguments { private static final IArguments EMPTY = new ObjectArguments(); + + private boolean released = false; private final List args; @Deprecated @@ -63,4 +67,34 @@ public final class ObjectArguments implements IArguments { return args.toArray(); } + + @Nonnull + @Override + public LuaTable getTableUnsafe( int index ) throws LuaException + { + if( released ) + { + throw new IllegalStateException( "Cannot use getTableUnsafe after IArguments has been released" ); + } + + return IArguments.super.getTableUnsafe( index ); + } + + @Nonnull + @Override + public Optional> optTableUnsafe( int index ) throws LuaException + { + if( released ) + { + throw new IllegalStateException( "Cannot use optTableUnsafe after IArguments has been released" ); + } + + return IArguments.super.optTableUnsafe( index ); + } + + @Override + public void releaseImmediate() + { + released = true; + } } diff --git a/src/main/java/dan200/computercraft/api/lua/ObjectLuaTable.java b/src/main/java/dan200/computercraft/api/lua/ObjectLuaTable.java new file mode 100644 index 000000000..fbc80fad1 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/lua/ObjectLuaTable.java @@ -0,0 +1,73 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2021. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ +package dan200.computercraft.api.lua; + +import javax.annotation.Nonnull; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +public class ObjectLuaTable implements LuaTable +{ + private final Map map; + + public ObjectLuaTable( Map map ) + { + this.map = Collections.unmodifiableMap( map ); + } + + @Override + public int size() + { + return map.size(); + } + + @Override + public boolean isEmpty() + { + return map.isEmpty(); + } + + @Override + public boolean containsKey( Object o ) + { + return map.containsKey( o ); + } + + @Override + public boolean containsValue( Object o ) + { + return map.containsKey( o ); + } + + @Override + public Object get( Object o ) + { + return map.get( o ); + } + + @Nonnull + @Override + public Set keySet() + { + return map.keySet(); + } + + @Nonnull + @Override + public Collection values() + { + return map.values(); + } + + @Nonnull + @Override + public Set> entrySet() + { + return map.entrySet(); + } +} diff --git a/src/main/java/dan200/computercraft/core/asm/Generator.java b/src/main/java/dan200/computercraft/core/asm/Generator.java index 4cd2d66cf..73e5ee392 100644 --- a/src/main/java/dan200/computercraft/core/asm/Generator.java +++ b/src/main/java/dan200/computercraft/core/asm/Generator.java @@ -130,8 +130,6 @@ public final class Generator private void addMethod( List> methods, Method method, LuaFunction annotation, PeripheralType genericType, T instance ) { - if( annotation.mainThread() ) instance = wrap.apply( instance ); - String[] names = annotation.value(); boolean isSimple = method.getReturnType() != MethodResult.class && !annotation.mainThread(); if( names.length == 0 ) @@ -183,6 +181,13 @@ public final class Generator } } + LuaFunction annotation = method.getAnnotation( LuaFunction.class ); + if( annotation.unsafe() && annotation.mainThread() ) + { + ComputerCraft.log.error( "Lua Method {} cannot use unsafe and mainThread", name ); + return Optional.empty(); + } + // We have some rather ugly handling of static methods in both here and the main generate function. Static methods // only come from generic sources, so this should be safe. Class target = Modifier.isStatic( modifiers ) ? method.getParameterTypes()[0] : method.getDeclaringClass(); @@ -190,11 +195,13 @@ public final class Generator try { String className = method.getDeclaringClass().getName() + "$cc$" + method.getName() + METHOD_ID.getAndIncrement(); - byte[] bytes = generate( className, target, method ); + byte[] bytes = generate( className, target, method, annotation.unsafe() ); if( bytes == null ) return Optional.empty(); Class klass = DeclaringClassLoader.INSTANCE.define( className, bytes, method.getDeclaringClass().getProtectionDomain() ); - return Optional.of( klass.asSubclass( base ).getDeclaredConstructor().newInstance() ); + + T instance = klass.asSubclass( base ).getDeclaredConstructor().newInstance(); + return Optional.of( annotation.mainThread() ? wrap.apply( instance ) : instance ); } catch( ReflectiveOperationException | ClassFormatError | RuntimeException e ) { @@ -205,7 +212,7 @@ public final class Generator } @Nullable - private byte[] generate( String className, Class target, Method method ) + private byte[] generate( String className, Class target, Method method, boolean unsafe ) { String internalName = className.replace( ".", "/" ); @@ -238,7 +245,7 @@ public final class Generator int argIndex = 0; for( java.lang.reflect.Type genericArg : method.getGenericParameterTypes() ) { - Boolean loadedArg = loadArg( mw, target, method, genericArg, argIndex ); + Boolean loadedArg = loadArg( mw, target, method, unsafe, genericArg, argIndex ); if( loadedArg == null ) return null; if( loadedArg ) argIndex++; } @@ -285,7 +292,7 @@ public final class Generator return cw.toByteArray(); } - private Boolean loadArg( MethodVisitor mw, Class target, Method method, java.lang.reflect.Type genericArg, int argIndex ) + private Boolean loadArg( MethodVisitor mw, Class target, Method method, boolean unsafe, java.lang.reflect.Type genericArg, int argIndex ) { if( genericArg == target ) { @@ -324,7 +331,7 @@ public final class Generator return true; } - String name = Reflect.getLuaName( Primitives.unwrap( klass ) ); + String name = Reflect.getLuaName( Primitives.unwrap( klass ), unsafe ); if( name != null ) { mw.visitVarInsn( ALOAD, 2 + context.size() ); @@ -344,7 +351,7 @@ public final class Generator return true; } - String name = arg == Object.class ? "" : Reflect.getLuaName( arg ); + String name = arg == Object.class ? "" : Reflect.getLuaName( arg, unsafe ); if( name != null ) { if( Reflect.getRawType( method, genericArg, false ) == null ) return null; diff --git a/src/main/java/dan200/computercraft/core/asm/Reflect.java b/src/main/java/dan200/computercraft/core/asm/Reflect.java index 4517fecc3..b7928d4c0 100644 --- a/src/main/java/dan200/computercraft/core/asm/Reflect.java +++ b/src/main/java/dan200/computercraft/core/asm/Reflect.java @@ -6,6 +6,7 @@ package dan200.computercraft.core.asm; import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.lua.LuaTable; import org.objectweb.asm.MethodVisitor; import javax.annotation.Nullable; @@ -25,7 +26,7 @@ final class Reflect } @Nullable - static String getLuaName( Class klass ) + static String getLuaName( Class klass, boolean unsafe ) { if( klass.isPrimitive() ) { @@ -39,6 +40,7 @@ final class Reflect if( klass == Map.class ) return "Table"; if( klass == String.class ) return "String"; if( klass == ByteBuffer.class ) return "Bytes"; + if( klass == LuaTable.class && unsafe ) return "TableUnsafe"; } return null; diff --git a/src/main/java/dan200/computercraft/core/lua/BasicFunction.java b/src/main/java/dan200/computercraft/core/lua/BasicFunction.java index f515cd455..5749a79cf 100644 --- a/src/main/java/dan200/computercraft/core/lua/BasicFunction.java +++ b/src/main/java/dan200/computercraft/core/lua/BasicFunction.java @@ -59,6 +59,10 @@ class BasicFunction extends VarArgFunction } throw new LuaError( "Java Exception Thrown: " + t, 0 ); } + finally + { + arguments.releaseImmediate(); + } if( results.getCallback() != null ) { diff --git a/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java b/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java index c6b2932e7..d76f27b47 100644 --- a/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java +++ b/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java @@ -15,6 +15,7 @@ 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.LuaTable; import org.squiddev.cobalt.compiler.CompileException; import org.squiddev.cobalt.compiler.LoadState; import org.squiddev.cobalt.debug.DebugFrame; diff --git a/src/main/java/dan200/computercraft/core/lua/ResultInterpreterFunction.java b/src/main/java/dan200/computercraft/core/lua/ResultInterpreterFunction.java index 495d1a2d1..ffc2ea444 100644 --- a/src/main/java/dan200/computercraft/core/lua/ResultInterpreterFunction.java +++ b/src/main/java/dan200/computercraft/core/lua/ResultInterpreterFunction.java @@ -69,6 +69,10 @@ class ResultInterpreterFunction extends ResumableVarArgFunction +{ + private final VarargArguments arguments; + private final LuaTable table; + private Map backingMap; + + TableImpl( VarargArguments arguments, LuaTable table ) + { + this.arguments = arguments; + this.table = table; + } + + @Override + public int size() + { + checkValid(); + try + { + return table.keyCount(); + } + catch( LuaError e ) + { + throw new IllegalStateException( e ); + } + } + + @Override + public int length() + { + return table.length(); + } + + @Override + public long getLong( int index ) throws LuaException + { + LuaValue value = table.rawget( index ); + if( !(value instanceof LuaNumber) ) throw LuaValues.badTableItem( index, "number", value.typeName() ); + if( value instanceof LuaInteger ) return value.toInteger(); + + double number = value.toDouble(); + if( !Double.isFinite( number ) ) throw badTableItem( index, "number", getNumericType( number ) ); + return (long) number; + } + + @Override + public boolean isEmpty() + { + checkValid(); + try + { + return table.next( Constants.NIL ).first().isNil(); + } + catch( LuaError e ) + { + throw new IllegalStateException( e ); + } + } + + @Nonnull + private LuaValue getImpl( Object o ) + { + checkValid(); + if( o instanceof String ) return table.rawget( (String) o ); + if( o instanceof Integer ) return table.rawget( (Integer) o ); + return Constants.NIL; + } + + @Override + public boolean containsKey( Object o ) + { + return !getImpl( o ).isNil(); + } + + @Override + public Object get( Object o ) + { + return CobaltLuaMachine.toObject( getImpl( o ), null ); + } + + @Nonnull + private Map getBackingMap() + { + checkValid(); + if( backingMap != null ) return backingMap; + return backingMap = Collections.unmodifiableMap( + Objects.requireNonNull( (Map) CobaltLuaMachine.toObject( table, null ) ) + ); + } + + @Override + public boolean containsValue( Object o ) + { + return getBackingMap().containsKey( o ); + } + + @Nonnull + @Override + public Set keySet() + { + return getBackingMap().keySet(); + } + + @Nonnull + @Override + public Collection values() + { + return getBackingMap().values(); + } + + @Nonnull + @Override + public Set> entrySet() + { + return getBackingMap().entrySet(); + } + + private void checkValid() + { + if( arguments.released ) + { + throw new IllegalStateException( "Cannot use LuaTable after IArguments has been released" ); + } + } +} diff --git a/src/main/java/dan200/computercraft/core/lua/VarargArguments.java b/src/main/java/dan200/computercraft/core/lua/VarargArguments.java index 0079e41c7..dc7eb5e2c 100644 --- a/src/main/java/dan200/computercraft/core/lua/VarargArguments.java +++ b/src/main/java/dan200/computercraft/core/lua/VarargArguments.java @@ -19,6 +19,7 @@ class VarargArguments implements IArguments { static final IArguments EMPTY = new VarargArguments( Constants.NONE ); + boolean released; private final Varargs varargs; private Object[] cache; @@ -98,4 +99,39 @@ class VarargArguments implements IArguments LuaString str = ((LuaBaseString) value).strvalue(); return Optional.of( ByteBuffer.wrap( str.bytes, str.offset, str.length ).asReadOnlyBuffer() ); } + + @Nonnull + @Override + public dan200.computercraft.api.lua.LuaTable getTableUnsafe( int index ) throws LuaException + { + if( released ) + { + throw new IllegalStateException( "Cannot use getTableUnsafe after IArguments has been released" ); + } + + LuaValue value = varargs.arg( index + 1 ); + if( !(value instanceof LuaTable) ) throw LuaValues.badArgument( index, "table", value.typeName() ); + return new TableImpl( this, (LuaTable) value ); + } + + @Nonnull + @Override + public Optional> optTableUnsafe( int index ) throws LuaException + { + if( released ) + { + throw new IllegalStateException( "Cannot use optTableUnsafe after IArguments has been released" ); + } + + LuaValue value = varargs.arg( index + 1 ); + if( value.isNil() ) return Optional.empty(); + if( !(value instanceof LuaTable) ) throw LuaValues.badArgument( index, "table", value.typeName() ); + return Optional.of( new TableImpl( this, (LuaTable) value ) ); + } + + @Override + public void releaseImmediate() + { + released = true; + } } diff --git a/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java b/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java index 18d04dac8..a0adb6250 100644 --- a/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java +++ b/src/test/java/dan200/computercraft/core/ComputerTestDelegate.java @@ -97,7 +97,7 @@ public class ComputerTestDelegate if( REPORT_PATH.delete() ) ComputerCraft.log.info( "Deleted previous coverage report." ); - Terminal term = new Terminal( 80, 30 ); + Terminal term = new Terminal( 80, 100 ); IWritableMount mount = new FileMount( new File( "test-files/mount" ), 10_000_000 ); // Remove any existing files diff --git a/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java b/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java index 21aec5288..7b817b14e 100644 --- a/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java +++ b/src/test/java/dan200/computercraft/core/asm/GeneratorTest.java @@ -120,6 +120,13 @@ public class GeneratorTest contramap( notNullValue(), "callback", MethodResult::getCallback ) ); } + @Test + public void testUnsafe() + { + List> methods = LuaMethod.GENERATOR.getMethods( Unsafe.class ); + assertThat( methods, contains( named( "withUnsafe" ) ) ); + } + public static class Basic { @LuaFunction @@ -222,6 +229,21 @@ public class GeneratorTest {} } + public static class Unsafe + { + @LuaFunction( unsafe = true ) + public final void withUnsafe( LuaTable table ) + {} + + @LuaFunction + public final void withoutUnsafe( LuaTable table ) + {} + + @LuaFunction( unsafe = true, mainThread = true ) + public final void invalid( LuaTable table ) + {} + } + private static T find( Collection> methods, String name ) { return methods.stream()