HaxeFoundation / haxe

Haxe - The Cross-Platform Toolkit
https://haxe.org
6.17k stars 654 forks source link

[jvm] ExceptionInInitializerError #11236

Open acarioni opened 1 year ago

acarioni commented 1 year ago

I installed haxe 4.3.1 for a project that previously used haxe 4.3.0-rc.1+966864c. It works well on every target but java, where it throws the exception below. Haxe 4.3.0 has the same problem too.

Unfortunately I am not able to reproduce the error by means of a simple test, but the basic execution flow is trivial: I create an instance of hx.concurrent.executor.Executor in a static variable and, when I try to call the method submit on it, I get ExceptionInInitializerError.

I already contacted the author of the library haxe-concurrent and his opinion is that the exception is due to a bug of the jvm target.

java.lang.ExceptionInInitializerError
          at utest.Test.delay(test-util/utest/Test.hx:152)
          at utest.Runner.runCurrent(test-util/utest/Runner.hx:129)
          at utest.Runner.gotoFirstTest(test-util/utest/Runner.hx:85)
          at utest.Runner$Closure_evtRun_0.invoke(test-util/utest/Runner.hx:60)
          at utest.Runner$Closure_evtRun_0.invoke(test-util/utest/Runner.hx)
          at hx.concurrent.lock.AbstractAcquirable.execute(/Users/acarioni/haxe/haxe_libraries/haxe-concurrent/5.1.3/haxelib/src/hx/concurrent/lock/Acquirable.hx:55)
          at utest.Runner.evtRun(test-util/utest/Runner.hx:55)
          at utest.Runner$Closure_run_0.invoke(test-util/utest/Runner.hx:52)
          at utest.Runner$Closure_run_0.invoke(test-util/utest/Runner.hx)
          at hx.concurrent.lock.AbstractAcquirable.execute(/Users/acarioni/haxe/haxe_libraries/haxe-concurrent/5.1.3/haxelib/src/hx/concurrent/lock/Acquirable.hx:55)
          at utest.Runner.run(test-util/utest/Runner.hx:50)
          at haxe.root.TestAll.main(test/TestAll.hx:93)
          at haxe.root.TestAll.main(test/TestAll.hx:1)
      Caused by: java.lang.ClassCastException: class hx.concurrent.thread.ThreadPool$Closure_onStart_0 cannot be cast to class java.lang.Runnable (hx.concurrent.thread.ThreadPool$Closure_onStart_0 is in unnamed module of loader 'app'; java.lang.Runnable is in module java.base of loader 'bootstrap')
          at sys.thread.Thread$NativeHaxeThread.<init>(/Users/acarioni/haxe/versions/4.3.1/std/java/_std/sys/thread/Thread.hx:160)
          at sys.thread.Thread$HaxeThread.create(/Users/acarioni/haxe/versions/4.3.1/std/java/_std/sys/thread/Thread.hx:104)
          at hx.concurrent.thread.ThreadPool.onStart(/Users/acarioni/haxe/haxe_libraries/haxe-concurrent/5.1.3/haxelib/src/hx/concurrent/thread/ThreadPool.hx:102)
          at hx.concurrent.ServiceBase$Closure_start_0.invoke(/Users/acarioni/haxe/haxe_libraries/haxe-concurrent/5.1.3/haxelib/src/hx/concurrent/Service.hx:65)
          at hx.concurrent.ServiceBase$Closure_start_0.invoke(/Users/acarioni/haxe/haxe_libraries/haxe-concurrent/5.1.3/haxelib/src/hx/concurrent/Service.hx)
          at hx.concurrent.lock.AbstractAcquirable.execute(/Users/acarioni/haxe/haxe_libraries/haxe-concurrent/5.1.3/haxelib/src/hx/concurrent/lock/Acquirable.hx:55)
          at hx.concurrent.ServiceBase.start(/Users/acarioni/haxe/haxe_libraries/haxe-concurrent/5.1.3/haxelib/src/hx/concurrent/Service.hx:58)
          at hx.concurrent.thread.ThreadPool.<init>(/Users/acarioni/haxe/haxe_libraries/haxe-concurrent/5.1.3/haxelib/src/hx/concurrent/thread/ThreadPool.hx:61)
          at hx.concurrent.executor.ThreadPoolExecutor.<init>(/Users/acarioni/haxe/haxe_libraries/haxe-concurrent/5.1.3/haxelib/src/hx/concurrent/executor/ThreadPoolExecutor.hx:41)
          at hx.concurrent.executor.Executor.create(/Users/acarioni/haxe/haxe_libraries/haxe-concurrent/5.1.3/haxelib/src/hx/concurrent/executor/Executor.hx:36)
          at utest.Test$Test_Fields_.<clinit>(test-util/utest/Test.hx:10)
          ... 13 more
acarioni commented 1 year ago

I add some more context.

The code emitted by haxe 4.3.0-rc.1, after being decompiled, is

public void onStart() {
  this.set_state(ServiceState.RUNNING);
  ThreadPool _gthis = this;
  int _g = 0;
  int _g1 = this.threadCount;

  while (_g < _g1) {
    ++_g;
    HaxeThread.create((Function) (new ThreadPool.Closure_onStart_0(_gthis)), false);
  }

}

public static class Closure_onStart_0 extends Function implements Runnable {
  /*...*/
}

And this is the code from haxe 4.3.1

public void onStart() {
  this.set_state(ServiceState.RUNNING);
  ThreadPool _gthis = this;
  int _g = 0;
  int _g1 = this.threadCount;

  while (_g < _g1) {
    ++_g;
    HaxeThread.create((Function) (new ThreadPool.Closure_onStart_0(_gthis)), false);
  }

}

public static class Closure_onStart_0 extends Function
    implements
      PrivilegedAction<Object>,
      PrivilegedExceptionAction<Object>,
      Callable<Object>,
      Supplier<Object>,
      Function0<Object> {
  /*...*/
}

As you can see, the class Closure_onStart_0 doesn’t implement the interface Runnable, as reported by the exception ClassCastException.

Simn commented 1 year ago

This very likely comes from https://github.com/HaxeFoundation/haxe/pull/11019. The Runnable interface used to be hardcoded, now the compiler relies on checking all known types. The behavior here suggests that the compiler either doesn't see Runnable or doesn't deem the closure compatible with it.

Not sure how to debug this without a reproducible example.

acarioni commented 1 year ago

The test case was too big to paste it here, but the bug can be reproduced.

You have to follow these steps:

  1. clone this project
  2. install a few tools to build the tests as listed here (you only need Apache Ant)
  3. go to the folder tools/java and issue the command ant test -DUTEST_PATTERN='TestEventDispatcher.testAddListener\b'

If you need any help, I can assist you in the configuration of the test environment.

acarioni commented 1 year ago

After the addition of the functional interfaces, a closure, that used to be a simple class extending Function and implementing Runnable, now is a monster implementing a dozen interfaces (see below).

Is this mess really necessary?

And the implementation is questionable too. For example the methods run and close execute the same code, which is indeed strange.

public static class Closure_onStart_0 extends Function
implements
  PrivilegedExceptionAction<Object>,
  PrivilegedAction<Object>,
  Callable<Object>,
  Supplier<Object>,
  Runnable,
  AutoCloseable,
  Closeable,
  Flushable,
  ObjectInputValidation,
  AsynchronousChannel,
  InterruptibleChannel {
        public final ThreadPool _gthis;

        public Closure_onStart_0(ThreadPool _gthis) {
            this._gthis = _gthis;
        }

        public void invoke() {
            // ...
        }

        public void close() {
            this.invoke();
        }

        public void validateObject() {
            this.invoke();
        }

        public void flush() {
            this.invoke();
        }

        public void run() {
            this.invoke();
        }

        public Object get() {
            return this.invoke();
        }

        public Object call() {
            return this.invoke();
        }

        public Object run() {
            return this.invoke();
        }
    }
acarioni commented 1 year ago

🎉 I finally managed to create a straightforward test case. The code below was run with Haxe 4.3.1 and target jvm.

import sys.thread.Thread;

function main() {
  var f = () -> 0;
  Thread.create(f);
}
Exception in thread "main" java.lang.ClassCastException: class foo._Main.Main_Fields_$Closure_main_f_0 cannot be cast to class java.lang.Runnable (foo._Main.Main_Fields_$Closure_main_f_0 is in unnamed module of loader 'app'; java.lang.Runnable is in module java.base of loader 'bootstrap')
        at sys.thread.Thread$NativeHaxeThread.<init>(/Users/acarioni/haxe/versions/4.3.1/std/java/_std/sys/thread/Thread.hx:160)
        at sys.thread.Thread$HaxeThread.create(/Users/acarioni/haxe/versions/4.3.1/std/java/_std/sys/thread/Thread.hx:104)
        at foo._Main.Main_Fields_.main(src/foo/Main.hx:7)
        at foo._Main.Main_Fields_.main(src/foo/Main.hx:1)
Simn commented 1 year ago

Hmm yeah, this is because the function returns something while Runnable expects a Void return. I'm not entirely sure if that should work, but it's a bit unfortunate because Haxe admits the assignment.

Also, are you sure that this particular example used to work before? I don't see how the previous implementation would support this either.

acarioni commented 1 year ago

This particular example doesn’t work on older versions of haxe too (the compiler accepts the code but then it throws a ClassCastException).

However, for some strange reasons, haxe 4.3.0-rc.1 generates the right code when I compile my open source project with it (see my previous post).

Is there a chance you can look at my project? The issue can be systematically reproduced when the code is compiled with haxe 4.3.1.

acarioni commented 1 year ago

And how about the code below?

function fun() return 0;

function main() {
  var f = () -> {
    fun();
  }
  Thread.create(f);
  Sys.sleep(0.5);
}

It’s hard to argue that it shouldn’t work only because the last statement in the function block happens to return a value as a side effect.

Simn commented 1 year ago

I think this should fail properly during typing because it's simply not natively supported.

acarioni commented 1 year ago

I have created a zip file that contains everything you need to reproduce the issue. You only have to install the lix package manager and run the included hxml file to trigger the bug. A readme file in the zip folder summarizes the steps for your convenience. haxe-issue.zip

Simn commented 1 year ago

Thanks! I know how to reproduce the issue, I just don't know what to do about it other than making it fail during typing.

Simn commented 1 year ago

Actually I don't even really know how to do that because all the compiler sees is a -> Something to -> Void assignment and it usually allows that. We would need some sort of ReallyVoid type to communicate the correct type relationship here...

acarioni commented 1 year ago

Is there any possibility of introducing a flag to disable support for functional interfaces at least until they work in the expected way?

acarioni commented 11 months ago

Please, consider the following program:

import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.lang.Runnable;

final exec = Executors.newSingleThreadScheduledExecutor();
function schedule(f: ()->Void) exec.schedule((cast f: Runnable), 0, TimeUnit.MILLISECONDS);
function greeter(): Void trace("hello");

function main() {
  schedule(greeter);
}

When compiled with Haxe 4.3.3 and executed, it throws the error _Exception in thread "main" java.lang.ClassCastException: class foo._Main.MainFields$Main_Fields_greeter cannot be cast to class java.lang.Runnable.

How can I help the compiler to generate the correct jvm types?

Simn commented 9 months ago

That example works on both development and with #11544. It hangs at run-time, which I'm not sure is expected.

acarioni commented 9 months ago

Yes, it is expected because the executor creates a non-daemon thread.

Simn commented 9 months ago

Right, I see. Could you turn this into something that doesn't hang so that I can add it as a test (ideally without any sleeping)? I'd like to make sure that we don't break this again.

acarioni commented 9 months ago

This should work. I added a shutdown to kill the executor after completing the task.

import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.lang.Runnable;

final exec = Executors.newSingleThreadScheduledExecutor();
function schedule(f: ()->Void) exec.schedule((cast f: Runnable), 0, TimeUnit.MILLISECONDS);
function greeter(): Void {
  trace("hello");
  exec.shutdown();
}

function main() {
  schedule(greeter);
}
Simn commented 9 months ago

Thanks! Added to #11544, without the cast because that seems to just work now. If you have any other cases that should be added as a test please let me know.

I'd also appreciate if you could test your code against that PR (once CI behaves and we get builds) to make sure we're not breaking something else. There's not a whole lot of functional interface testing other than what @EliteMasterEric has been doing, so at the moment it's somewhat difficult to stabilize this.

acarioni commented 9 months ago

I tested my project with haxe 5.0.0-alpha.1+7aab7b5. The exception ExceptionInInitializerError didn’t occur anymore and all the tests passed. However, I had to rewrite some expressions that used operator overloading, because the compiler could not resolve them.

Simn commented 9 months ago

Thanks for testing! I'm not aware of any deliberate changes to operator overloading, but there's always a chance that some bugfix affected this. If you can isolate something, please open an issue.