Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[jvm] Functional Interfaces not extending proper interface in some cases #11054

Closed
EliteMasterEric opened this issue Mar 29, 2023 · 18 comments
Closed
Labels
platform-jvm Everything related to JVM waiting-for-feedback We need more information to deal with this issue.
Milestone

Comments

@EliteMasterEric
Copy link
Contributor

I just recently got around to testing, and although the examples work in pure Haxe, they do not work when trying to interact with an extern which is using functional interfaces.

I got this crash report:

Caused by: java.lang.ClassCastException: class com.elitemastereric.obsidianarmor.items.ModItems_onInitialize_77__Fun cannot be cast to class net.fabricmc.fabric.api.itemgroup.v1.ItemGroupEvents$ModifyEntries

onInitialize()#77 is a Haxe lambda of type (FabricItemGroupEntries) -> Void, and looks like this:

    ItemGroupEvents.modifyEntriesEvent(ITEM_GROUP).register(function(content:FabricItemGroupEntries) {
      content.add(OBSIDIAN_AXE);
      content.add(OBSIDIAN_PICKAXE);
      content.add(OBSIDIAN_SHOVEL);
      content.add(OBSIDIAN_SWORD);
      content.add(OBSIDIAN_HOE);

      content.add(OBSIDIAN_HELMET);
      content.add(OBSIDIAN_CHESTPLATE);
      content.add(OBSIDIAN_LEGGINGS);
      content.add(OBSIDIAN_BOOTS);
    });

and the generated Java code:

net.fabricmc.fabric.api.itemgroup.v1.ItemGroupEvents.modifyEntriesEvent(((net.minecraft.world.item.CreativeModeTab) (com.elitemastereric.obsidianarmor.items.ModItems.ITEM_GROUP) )).register(((net.fabricmc.fabric.api.itemgroup.v1.ItemGroupEvents.ModifyEntries) (((java.lang.Object) (( (( com.elitemastereric.obsidianarmor.items.ModItems_onInitialize_77__Fun.__hx_current != null )) ? (com.elitemastereric.obsidianarmor.items.ModItems_onInitialize_77__Fun.__hx_current) : (com.elitemastereric.obsidianarmor.items.ModItems_onInitialize_77__Fun.__hx_current = ((com.elitemastereric.obsidianarmor.items.ModItems_onInitialize_77__Fun) (new com.elitemastereric.obsidianarmor.items.ModItems_onInitialize_77__Fun()) )) )) )) ));

register() takes a ModifyEntries, which is an inner interface of FabricItemGroupEntries, whose full source is:

	@FunctionalInterface
	public interface ModifyEntries {
		/**
		 * Modifies the item group entries.
		 * @param entries the entries
		 * @see FabricItemGroupEntries
		 */
		void modifyEntries(FabricItemGroupEntries entries);
	}

It appears that in the generated Java, ModItems_onInitialize_77__Fun is a haxe.lang.Function which is attempted to be cast to ModifyEntries, which fails.

My expectation is that, since ModItems_onInitialize_77__Fun is passed as an argument to register and cast to the ModifyEntries type in the generated Java source code, it should be compatible with that class and the lambda function should be able to be properly called. The result is very similar to the pure Java syntax for this example, which looks like this:

        ItemGroupEvents.modifyEntriesEvent(ITEM_GROUP).register(content -> {
            content.accept(OBSIDIAN_AXE);
            content.accept(OBSIDIAN_PICKAXE);
            content.accept(OBSIDIAN_SHOVEL);
            content.accept(OBSIDIAN_SWORD);
            content.accept(OBSIDIAN_HOE);

            content.accept(OBSIDIAN_HELMET);
            content.accept(OBSIDIAN_CHESTPLATE);
            content.accept(OBSIDIAN_LEGGINGS);
            content.accept(OBSIDIAN_BOOTS);
        });

The lambda function is casted to a ModifyEntries without issue.

Tied to #11014, but I decided this is sufficiently different enough to be its own issue (the previous issue was about functional interfaces not being a thing in general, this one is about their interaction with Java source/extern libraries).

@Simn Simn added the platform-jvm Everything related to JVM label Mar 29, 2023
@Simn
Copy link
Member

Simn commented Mar 29, 2023

A basic example works for me:

package p;

@FunctionalInterface
public interface ModifyEntries {
	/**
		* Modifies the item group entries.
		* @param entries the entries
		* @see FabricItemGroupEntries
		*/
	void modifyEntries(int entries);
}

If I build that using javac -d export source/p/ModifyEntries.java -g and jar cf it, the following works with --java-lib:

function test(m:p.ModifyEntries) {
	m.modifyEntries(12);
}

function main() {
	test(i -> trace(12));
}

I suppose we'll need a reproducible example here. Is there a simple .jar I can test this with or does that have 200 dependencies?

@EliteMasterEric
Copy link
Contributor Author

I don't have a simple JAR to test with yet, sadly. My project will be public soon (I used the workaround of creating a Haxe class that implements the functional interface), so it might be possible to do some more testing with that.

What does the generated Java source for the lambda function on line 6 look like?

@Simn
Copy link
Member

Simn commented Mar 29, 2023

public final class Main_Fields_ extends Object {
    public static void main(String[] args) {
        Init.init();
        main();
    }

    public static void test(ModifyEntries m) {
        m.modifyEntries(12);
    }

    public static void main() {
        test((ModifyEntries)Main_Fields_.Closure_main_0.Main_Fields_$Closure_main_0);
    }

    public static class Closure_main_0 extends Function implements ModifyEntries {
        public static final Main_Fields_.Closure_main_0 Main_Fields_$Closure_main_0 = new Main_Fields_.Closure_main_0();

        Closure_main_0() {
        }

        public void invoke(int i) {
            ((Function)Log.trace).invoke(12, new Anon0("_Main.Main_Fields_", "source/Main.hx", 6, "main"));
        }

        public void modifyEntries(int arg0) {
            this.invoke(arg0);
        }
    }
}

@EliteMasterEric
Copy link
Contributor Author

    public static class Closure_main_0 extends Function implements ModifyEntries {
        public void modifyEntries(int arg0) {
            this.invoke(arg0);
        }
    }

Very interesting. In my codebase, the function generates in its own Java file, and does not include the ModifyEntries interface.

I'm trying to think of potentially complicating factors. Maybe my build script is accidentally using the wrong version of Haxe, or maybe it's an issue with the fact that ModifyEntries is an inner class like this:

public final class ItemGroupEvents {
	@FunctionalInterface
	public interface ModifyEntries {
		/**
		 * Modifies the item group entries.
		 * @param entries the entries
		 * @see FabricItemGroupEntries
		 */
		void modifyEntries(FabricItemGroupEntries entries);
	}
}

@Simn
Copy link
Member

Simn commented Mar 29, 2023

It being an inner class could indeed be related, perhaps the generator doesn't see the interface and thus never processes it.

Simn added a commit that referenced this issue Mar 31, 2023
@Simn
Copy link
Member

Simn commented Mar 31, 2023

I have added a test case which works for me, see linked commit. This is either some setup case on your end or there must be some other complicating factor in play.

@Simn Simn added this to the Later milestone Mar 31, 2023
@Simn Simn added the waiting-for-feedback We need more information to deal with this issue. label Mar 31, 2023
@EliteMasterEric
Copy link
Contributor Author

I've reproduced the issue, this time with java.util.function.Consumer.

My test project is public now, here is the branch/sample the issue occurs on:

https://github.com/EliteMasterEric/PickHaxe/blob/feature/forge-support/src/net/pickhaxe/core/CommonMod.hx#L85

Apologies, the project has some boilerplate to it:

  • Download the project at that branch, you'll also need JDK 17 I think.
  • Add the project as the pickhaxe Haxelib.
  • Open the samples/made-in-haxe folder.
  • Run haxelib run pickhaxe build forge 1.19.3. This creates (and also executes) a ./generated/build.hxml file (which you can safely re-run).
  • Check ./generated/build/minecraft/forge-1.19.3-44.1.23_mapped_parchment_2023.03.12-1.19.3.jar and ./generated/build/minecraft/eventbus-6.0.3.jar for the library I am linking against here.

I am attempting to register 3 event listener functions to the bus, and the generated code looks like this:

	public void forge_registerListeners()
	{
		//line 78 "E:\\Programming\\Game Modding\\Minecraft Modding\\PickHaxe\\pickhaxe\\src\\net\\pickhaxe\\core\\CommonMod.hx"
		net.pickhaxe.core.CommonMod _gthis = this;
		//line 85 "E:\\Programming\\Game Modding\\Minecraft Modding\\PickHaxe\\pickhaxe\\src\\net\\pickhaxe\\core\\CommonMod.hx"
		this.modEventBus.addListener(((java.util.function.Consumer<net.minecraftforge.registries.NewRegistryEvent>) (((java.lang.Object) (new net.pickhaxe.core.CommonMod_forge_registerListeners_85__Fun(_gthis)) )) ));
		//line 85 "E:\\Programming\\Game Modding\\Minecraft Modding\\PickHaxe\\pickhaxe\\src\\net\\pickhaxe\\core\\CommonMod.hx"
		java.lang.Object __temp_expr1 = ((java.lang.Object) (null) );
		//line 90 "E:\\Programming\\Game Modding\\Minecraft Modding\\PickHaxe\\pickhaxe\\src\\net\\pickhaxe\\core\\CommonMod.hx"
		this.modEventBus.addListener(((java.util.function.Consumer<net.minecraftforge.registries.RegisterEvent>) (((java.lang.Object) (new net.pickhaxe.core.CommonMod_forge_registerListeners_90__Fun(_gthis)) )) ));
		//line 90 "E:\\Programming\\Game Modding\\Minecraft Modding\\PickHaxe\\pickhaxe\\src\\net\\pickhaxe\\core\\CommonMod.hx"
		java.lang.Object __temp_expr2 = ((java.lang.Object) (null) );
		//line 95 "E:\\Programming\\Game Modding\\Minecraft Modding\\PickHaxe\\pickhaxe\\src\\net\\pickhaxe\\core\\CommonMod.hx"
		this.modEventBus.addListener(((java.util.function.Consumer<net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent>) (((java.lang.Object) (new net.pickhaxe.core.CommonMod_forge_registerListeners_95__Fun(_gthis)) )) ));
		//line 95 "E:\\Programming\\Game Modding\\Minecraft Modding\\PickHaxe\\pickhaxe\\src\\net\\pickhaxe\\core\\CommonMod.hx"
		java.lang.Object __temp_expr3 = ((java.lang.Object) (null) );
	}

However, the functions do not implement the Consumer interface, and therefore do not implement the accept function. The classes compile, but I get:

	Exception message: java.lang.ClassCastException: class net.pickhaxe.core.CommonMod_forge_registerListeners_85__Fun cannot be cast to class java.util.function.Consumer (net.pickhaxe.core.CommonMod_forge_registerListeners_85__Fun is in module [email protected] of loader 'TRANSFORMER' @2100d047; java.util.function.Consumer is in module java.base of loader 'bootstrap')

It just occurred to me that my current Haxe version (4.3.0-rc.1+f132e61) may be too old, I will reinstall and test again.

@EliteMasterEric
Copy link
Contributor Author

Can confirm the listeners don't properly implement the interface on 4.3.0-rc.1+68280ac

@Simn
Copy link
Member

Simn commented Apr 3, 2023

I can take a look at this (at some point), but it will probably be addressed faster if you reduce this to a minimal example yourself.

@EliteMasterEric
Copy link
Contributor Author

EliteMasterEric commented Apr 3, 2023

Thanks, I can try to make a minimal example when i get the time

@Simn
Copy link
Member

Simn commented Apr 9, 2023

I just tried following these steps, but I'm having setup issues:

Performing Haxe build...
Copying resources...
--java-lib: The type java.lang.Record is referred but was not found. Compilation may not occur correctly.
Did you forget to include a needed lib?
--java-lib: The type java.lang.Record is referred but was not found. Compilation may not occur correctly.
Did you forget to include a needed lib?
--java-lib: The type java.lang.Record is referred but was not found. Compilation may not occur correctly.
Did you forget to include a needed lib?
(unknown) : [PICKHAXE] Generating compile defines based on Minecraft 1.19.3
(unknown) : [PICKHAXE] Done generating compile defines.
C:\git\haxe\std/java/_std/haxe/Exception.hx:11: lines 11-91 : Warning : No overload found for this constructor call
C:\git\haxe\std/java/_std/haxe/Exception.hx:11: lines 11-91 : Warning : No overload found for this constructor call
C:\git\haxe\std/java/_std/haxe/Exception.hx:11: lines 11-91 : Warning : No overload found for this constructor call
C:\git\haxe\std/java/_std/haxe/Exception.hx:11: lines 11-91 : Warning : No overload found for this constructor call
C:\git\haxe\std/java/internal/HxObject.hx:36: characters 1-23 : Warning : No overload found for this constructor call
C:\git\haxe\std/java/internal/HxObject.hx:36: characters 1-23 : Warning : No overload found for this constructor call
C:\git\haxe\std/java/internal/HxObject.hx:36: characters 1-23 : Warning : No overload found for this constructor call
C:\git\haxe\std/java/internal/HxObject.hx:36: characters 1-23 : Warning : No overload found for this constructor call
C:\git\haxe\std/java/internal/HxObject.hx:32: characters 9-47 : Warning : No overload found for this constructor call
C:\git\haxe\std/java/internal/HxObject.hx:32: characters 9-47 : Warning : No overload found for this constructor call
C:\git\haxe\std/java/internal/HxObject.hx:32: characters 9-47 : Warning : No overload found for this constructor call
C:\git\haxe\std/java/internal/HxObject.hx:32: characters 9-47 : Warning : No overload found for this constructor call

Did you work around #11016 somehow? I'm hoping that these overload errors are just weird follow-up issues.

@EliteMasterEric
Copy link
Contributor Author

I just tried following these steps, but I'm having setup issues:

Performing Haxe build...
Copying resources...
--java-lib: The type java.lang.Record is referred but was not found. Compilation may not occur correctly.
Did you forget to include a needed lib?
--java-lib: The type java.lang.Record is referred but was not found. Compilation may not occur correctly.
Did you forget to include a needed lib?
--java-lib: The type java.lang.Record is referred but was not found. Compilation may not occur correctly.
Did you forget to include a needed lib?
(unknown) : [PICKHAXE] Generating compile defines based on Minecraft 1.19.3
(unknown) : [PICKHAXE] Done generating compile defines.
C:\git\haxe\std/java/_std/haxe/Exception.hx:11: lines 11-91 : Warning : No overload found for this constructor call
C:\git\haxe\std/java/_std/haxe/Exception.hx:11: lines 11-91 : Warning : No overload found for this constructor call
C:\git\haxe\std/java/_std/haxe/Exception.hx:11: lines 11-91 : Warning : No overload found for this constructor call
C:\git\haxe\std/java/_std/haxe/Exception.hx:11: lines 11-91 : Warning : No overload found for this constructor call
C:\git\haxe\std/java/internal/HxObject.hx:36: characters 1-23 : Warning : No overload found for this constructor call
C:\git\haxe\std/java/internal/HxObject.hx:36: characters 1-23 : Warning : No overload found for this constructor call
C:\git\haxe\std/java/internal/HxObject.hx:36: characters 1-23 : Warning : No overload found for this constructor call
C:\git\haxe\std/java/internal/HxObject.hx:36: characters 1-23 : Warning : No overload found for this constructor call
C:\git\haxe\std/java/internal/HxObject.hx:32: characters 9-47 : Warning : No overload found for this constructor call
C:\git\haxe\std/java/internal/HxObject.hx:32: characters 9-47 : Warning : No overload found for this constructor call
C:\git\haxe\std/java/internal/HxObject.hx:32: characters 9-47 : Warning : No overload found for this constructor call
C:\git\haxe\std/java/internal/HxObject.hx:32: characters 9-47 : Warning : No overload found for this constructor call

Did you work around #11016 somehow? I'm hoping that these overload errors are just weird follow-up issues.

These are merely warnings, the final product works fine right now. I ended up finding some kind of workaround for this I think

I need to set aside some time to isolate this issue for you, but it'll take messing with Java source as well as Haxe.

@EliteMasterEric
Copy link
Contributor Author

I re-discovered this issue after doing some more experimenting, and it seems #11014 didn't fix it, and I never actually put together a minimal reproduction for this one here, doh.

I suspect it has to do with either the use of type parameters or the use of inner functional interfaces. I will experiment with this.

@EliteMasterEric
Copy link
Contributor Author

After bashing my head against a sample repo for an hour, I've reproduced it.

https://github.com/EliteMasterEric/Issue11054/

Closure_main_1 should implement Robot.GreetRobot but does not.

I'm not confident what exactly is causing it, I'm using anonymous classes requesting functional interfaces with type parameters, so it could be any manner of things.

@EliteMasterEric EliteMasterEric changed the title [java] Functional Interfaces not working with JAR externs [jvm] Functional Interfaces not extending proper interface in some cases Nov 8, 2023
@Simn
Copy link
Member

Simn commented Nov 8, 2023

Ported to pure Haxe:

abstract class Robot<T> {
	public function new() {}

	public function performTask(listener:T) {
		trace("Robot.performTask() called!");
	}

	public function toString() {
		return "Robot";
	}
}

interface IGreetRobot {
	function greet<T>(robot:Robot<T>):Void;
}

interface IMathOperation {
	function operate(a:Int, b:Int):Int;
}

class MathRobot extends Robot<IMathOperation> {
	override function performTask(listener:IMathOperation) {
		super.performTask(listener);
		var result = listener.operate(3, 4);
		trace("Result: " + result);
	}
}

class GreetRobot extends Robot<IGreetRobot> {
	var target:Robot<Dynamic>;

	public function new(target:Robot<Dynamic>) {
		super();
		this.target = target;
	}

	override function performTask(listener:IGreetRobot) {
		super.performTask(listener);
		listener.greet(target);
	}
}

class Main {
	public static function main() {
		var robot1 = new MathRobot();
		var robot2 = new GreetRobot(robot1);

		robot1.performTask(add);
		robot1.performTask(function(a:Int, b:Int):Int {
			return a - b;
		});

		robot2.performTask(function(target) {
			trace('Hello, ${target.toString()}!');
		});
	}

	static function add(a:Int, b:Int):Int {
		return a + b;
	}
}
source/Main.hx:5: Robot.performTask() called!
source/Main.hx:25: Result: 7
source/Main.hx:5: Robot.performTask() called!
source/Main.hx:25: Result: -1
Exception in thread "main" java.lang.ClassCastException: class haxe.root.Main$Closure_main_1 cannot be cast to class haxe.root.IGreetRobot (haxe.root.Main$Closure_main_1 and haxe.root.IGreetRobot are in unnamed module of loader 'app')
        at haxe.root.Main.main(source/Main.hx:53)

My first thought was that it might be about target being of unknown type, but type-hinting it to Robot<Dynamic> still gives the same error. Not sure right now what's wrong here, but being able to reproduce it without anonymous class shenanigans is good news.

By the way, please remind me: If we have class Robot<T> in Java and refer to it as just Robot, what does that mean? Is it just Robot<Object>?

@EliteMasterEric
Copy link
Contributor Author

EliteMasterEric commented Nov 8, 2023

By the way, please remind me: If we have class Robot<T> in Java and refer to it as just Robot, what does that mean? Is it just Robot<Object>?

It ends up being what's known as a raw type. https://docs.oracle.com/javase/tutorial/java/generics/rawTypes.html

I believe these act WITHOUT type checking, so at compile time it just assumes the value you pass for T is valid and throws a runtime error if it's not valid.

You also get a warning in most IDEs.

image

The following is a quote from Effective Java 2nd Edition, Item 23: Don't use raw types in new code:

Just what is the difference between the raw type List and the parameterized type List? Loosely speaking, the former has opted out generic type checking, while the latter explicitly told the compiler that it is capable of holding objects of any type. While you can pass a List to a parameter of type List, you can't pass it to a parameter of type List. There are subtyping rules for generics, and List is a subtype of the raw type List, but not of the parameterized type List. As a consequence, you lose type safety if you use raw type like List, but not if you use a parameterized type like List.

@Simn
Copy link
Member

Simn commented Nov 8, 2023

This works now if target is properly typed. The problem with your example is that it becomes an anonymous type due to the toString() access. This makes it compatible as far as Haxe's unification logic is concerned, but not as far as Java's is. It's a similar problem to #11236, and I don't have a good solution for this (even detecting the stricter unification requirements is somewhat of a hassle).

So for now, it requires some awareness, but it should be easy to work around.

@EliteMasterEric
Copy link
Contributor Author

Personally, mandating that all method references used with functional interfaces are strongly typed is an acceptable compromise, and the newest commit (bdd7bf4 is the one I used) successfully resolves the issue for me as long as method arguments are typed.

If you want to resolve type inference on method arguments, I would say make a new issue for it, I'd say this one can properly be closed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
platform-jvm Everything related to JVM waiting-for-feedback We need more information to deal with this issue.
Projects
None yet
Development

No branches or pull requests

2 participants