ikvmnet / ikvm

A Java Virtual Machine and Bytecode-to-IL Converter for .NET
Other
1.22k stars 111 forks source link

TypeLoadException - Could not create array type #343

Open AliveDevil opened 1 year ago

AliveDevil commented 1 year ago
System.TypeLoadException
  HResult=0x80131522
  Nachricht = Could not create array type 'System.ReadOnlySpan`1[System.Char][]' from assembly 'System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e' because the element type is ByRef-like.
  Quelle = <Die Ausnahmequelle kann nicht ausgewertet werden.>
  Stapelüberwachung:
<Die Ausnahmestapelüberwachung kann nicht ausgewertet werden.>
   bei System.RuntimeTypeHandle.MakeSZArray()
   bei System.RuntimeType.MakeArrayType()
   bei IKVM.Internal.DotNetTypeWrapper.MakeMethodDescriptor(MethodBase mb, String& name, String& sig, TypeWrapper[]& args, TypeWrapper& ret)
   bei IKVM.Internal.DotNetTypeWrapper.LazyPublishMembers()
   bei IKVM.Internal.TypeWrapper.GetMethods()
   bei IKVM.Java.Externs.java.lang.Class.getDeclaredConstructors0(Class thisClass, Boolean publicOnly)
   bei java.lang.Class.getDeclaredConstructors0(Boolean publicOnly)
   bei java.lang.Class.privateGetDeclaredConstructors(Boolean publicOnly)
   bei java.lang.Class.getConstructor0(Class[] parameterTypes, Int32 which)
   bei java.lang.Class.getConstructor(Class[] parameterTypes, CallerID )
   bei org.apache.commons.lang3.reflect.ConstructorUtils.getMatchingAccessibleConstructor(Class cls, Class[] parameterTypes)
   bei ch.cyberduck.core.LocalFactory..ctor()
   bei ch.cyberduck.core.LocalFactory.get(String path)
   bei Ch.Cyberduck.Core.Preferences.RoamingSupportDirectoryFinder.find() in W:\iterate\cyberduck\core\src\main\csharp\ch\cyberduck\core\preferences\RoamingSupportDirectoryFinder.cs: Zeile30
   bei ch.cyberduck.core.preferences.PreferencesFactory.set(Preferences p)
   bei ch.cyberduck.cli.Terminal.open(String[] args, TerminalPreferences defaults)
   bei Ch.Cyberduck.Cli.WindowsTerminal.Main(String[] args) in W:\iterate\cyberduck\cli\src\main\csharp\ch\cyberduck\cli\WindowsTerminal.cs: Zeile39

Relevant Java code: https://github.com/iterate-ch/cyberduck/blob/90fdf1bdde637e6e33f2725e3e70b8a7650bebe4/core/src/main/java/ch/cyberduck/core/LocalFactory.java#L42-L49

Inspected .NET type: https://github.com/iterate-ch/cyberduck/blob/90fdf1bdde637e6e33f2725e3e70b8a7650bebe4/core/src/main/csharp/ch/cyberduck/core/local/SystemLocal.cs

Runtime: .NET 6.0 IKVM: custom-built, before hotfix/8.5.1.

wasabii commented 1 year ago

Thinking through the issue.

The problem is the Append method in your .NET class. That method accepts a first argument of type in ReadOnlySpan<char>. So, it's a by-ref argument of a ref-like type. IKVM here is trying to generate a corresponding Java signature to represent that method. An existing 'hack' exists in IKVM for by-ref type arguments: generate a signature that accepts an array instead. During compilation of Java code the call site then is rewritten to unpack the first argument of that array and pass that. So, Java code can call this method by creating a new array, setting the first element, then calling it. The generated IL uses the ldelema instruction to get a managed reference to that element, and pass that instead.

This hack works for most things. Except, in this case, ReadOnlySpan Because ReadOnlySpan is a ref-like structure. It cannot be placed in an array. This hack actually DOES work on .NET Framework, because .NET Framework has no restriction on ref-like types appearing on the heap. For Framework, the only thing that would stop this is the C# compiler.

So, we've found a bug that only breaks on Core, because of a new feature in Core.

Solutions:

a) Hide the method. This kind of sucks. The method is truly on the type, and should appear on the Class. b) Use a different signature type and call site rewriting for ref-like types.

Option (b) is preferable. It would allow Java code to actually call this method.

Option (b) comes in two parts: determining what the signature should be, and then the call site rewriting. We can solve your immediate problem by just doing the first half, setting us up to implement the second half later.

We need to decide what the signature should look like though. We can't use an array. We could use a new type: ikvm.lang.ByRef<?>. We would be implementing a new type for this. This would allow Java code to see signatures that accept a managed reference.

In terms of call site rewriting, we would rewrite usages of ikvm.lang.ByRef to equate to managed references. However, we can defer this to a later date, and have reflection working pretty easily.

wasabii commented 1 year ago

JDK9 introduced java.lang.invoke.VarHandle, which sort of plays the role of a managed reference. With one missing feature, it doesn't support references to local stack variables.

If we were to coopt this, the Append method would appear to be void Append(VarHandle<Span<char>> range, ...). However, one could never call this straight from Java, as one would have no way to create a VarHandle referencing a local variable. But that might be okay. We're not trying to introduce new language features into Java.

Thing though is we have already sort of implemented a new language feature into Java: you can new ValueType(). Java has no ValueTypes. But IKVM allows this. It does appear to box the value on return though. So it's never really a value type.

So how would a Java program use this:

public void foo() {
   VarHandle<int> h = ClrMethodWithRefReturn();
   Append(h, ...);
}

If the stack originates in Java, there'd be no way to get a real stack value. But that's normal. You can't. However, a user could use C#:

public void Foo()
{
   var s = array.AsSpan();
   JavaMethodThatTakesVarHandle(ref s);
}

The Java code now has a ref pointing to stack.