javaluatype-safetyluajlua-userdata

LuaJ array/list type safety


So using LuaJ.

If I pass, from Java to Lua, a userdata List<T> with type T, Luaj still allows insertion into that array of any type of object via the :add function. For example:

Java code:

import java.util.ArrayList;
import org.luaj.vm2.Globals;
import org.luaj.vm2.lib.jse.CoerceJavaToLua;
import org.luaj.vm2.lib.jse.JsePlatform;
import org.luaj.vm2.LuaValue;

ArrayList<Integer>ExampleList=new ArrayList<>();
ExampleList.add(1);
LuaValue[] LuaParams=new LuaValue[] {
    CoerceJavaToLua.coerce(ExampleList)
};

Globals globals=JsePlatform.standardGlobals();
try { globals.get("TestFunc").invoke(LuaValue.varargsOf(LuaParams)); }
catch(Exception e) {}

Lua:

function TestFunc(arr)
    arr:add("str")
    arr:add(2);
end

Result of ExampleList:

{
    new Integer(1),
    new String("str"), //This should not be allowed!
    new Integer(2)
}

That string should not have been allowed since ExampleList is a List<Integer>

Question: Is there any way to maintain type safety?

If it helps for testing, here is the code to add the lua script into lua memory (just before the try{}):

globals.load(
    "function TestFunc(arr)\n"+
    "        arr:add(\"str\")\n"+
    "        arr:add(2);\n"+
    "end",
"ExampleScript").call();

Solution

  • After doing the research, I've found that it is not possible to find out what generic type an array is declared as. Java does not store that information within the object. During runtime, it just uses the type the array is declared as for the current variable reference.

    All you can do is look at the objects inside of it to determine what it is supposed to be, but this is not foolproof.

    If the array is defined within another object, then you can look at the parent object's fields to get the component/template/generic type of the array.

    ArrayList reflection

    [edit on 2016-07-06] Another suggested method that I was aware of was extending all list classes with an interface which actually stores the type of class. This wouldn't really be practical though for the project. After having given it thought, it makes sense why Java does not store the generic class type for a list.

    The solution I ended up using was editing org.luaj.vm2.lib.jse.JavaMethod.invokeMethod(Object instance, Varargs args) with the following (after the Object[] a = convertArgs(args); line:

    //If this is adding/setting to a list, make sure the object type matches the list's 0th object type
    java.util.List TheInstanceList;
    if(
        instance instanceof java.util.List && //Object is a list
        java.util.Arrays.asList("add", "set").contains(method.getName()) && //Adding/setting to list
        (TheInstanceList=(java.util.List)instance).size()>0 && //List already has at least 1 item
        !a[a.length>1 ? 1 : 0].getClass().isInstance(TheInstanceList.get(0)) //New item does not match type of item #0
    )
        return LuaValue.error(String.format(
                "list coercion error: %s is not instanceof %s",
                a[a.length>1 ? 1 : 0].getClass().getName(),
                TheInstanceList.get(0).getClass().getName()
        ));
    

    While this could be extended to account for matching parent classes by traversing up both object's extended-parent-type-list (everything before java.lang.Object) that would be less safe type-safety-wise than what we need for the project.

    The solution I used from above is there specifically to weed out errors in LUA scripts before they are committed to production.

    We may also end up needing to make a hack in which certain classes are considered one of their ancestor or inheritance classes when comparing.

    [Edit on 2016-07-08] I ended up adding the ability to also have Lists with a declared type, so there is no type guessing required.

    Replacement code for the code block from above:

    //If this is adding/setting to a list, make sure the object has the proper class type
    if(
        instance instanceof java.util.List && //Object is a list
        java.util.Arrays.asList("add", "set").contains(method.getName()) //Adding/setting to list
    ) {
        //If this is a TypedList, use its stored class for the typecheck
        java.util.List TheInstanceList=(java.util.List)instance;
        Class ClassInstance=null;
        if(instance instanceof lua.TypedList)
            ClassInstance=((lua.TypedList)instance).GetListClass();
        //Otherwise, check for a 0th object to typecheck against
        else if(TheInstanceList.size()>0) //List already has at least 1 item
            ClassInstance=TheInstanceList.get(0).getClass(); //Class of the 0th item
    
        //Check if new item does not match found class type
        if(
            ClassInstance!=null && //Only check if there is a class to check against
            !ClassInstance.isInstance(a[a.length>1 ? 1 : 0]) //Check the last parameter's class
        )
            return LuaValue.error(String.format(
                    "list coercion error: %s is not instanceof %s",
                    a[a.length>1 ? 1 : 0].getClass().getName(),
                    ClassInstance.getName()
            ));
    }
    

    And the code for the TypedList:

    /**
     * This is a special List class used with LUA which tells LUA what the types of objects in its list must be instances of.
     * Otherwise, when updating a list in LUA, whatever is the first object in a list is what all other objects must be an instance of.
     */
    public interface TypedList {
        Class GetListClass();
    }
    

    Bare ArrayList as a TypeList:

    import java.util.ArrayList;
    
    public class TypedArrayList<E> extends ArrayList<E> implements TypedList {
        private Class ListType;
        public TypedArrayList(Class c) {
            DefaultConstructor(c);
        };
        public TypedArrayList(Class c, java.util.Collection<? extends E> collection) {
            super(collection);
            DefaultConstructor(c);
        }
        private void DefaultConstructor(Class c) { ListType=c; }
        @Override public Class GetListClass() {
            return ListType;
        }
    }