From c700138b666dc6126c8c93760e4b402ef96413f8 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Thu, 29 Oct 2026 01:49:27 -0400 Subject: [PATCH] Native provider loading - Native provider classes load from the provider jars before the plugin class loader - Another plugin bundling VolmLib natives no longer supplies Iris native capabilities on Spigot - Tests cover provider jar classes taking precedence over the plugin loader --- .../bootstrap/NativeRuntimeLibraries.java | 31 ++++++++++++++++-- .../bootstrap/NativeRuntimeLibrariesTest.java | 32 +++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/art/arcane/iris/platform/bootstrap/NativeRuntimeLibraries.java b/core/src/main/java/art/arcane/iris/platform/bootstrap/NativeRuntimeLibraries.java index 54619f8d9..f508befb4 100644 --- a/core/src/main/java/art/arcane/iris/platform/bootstrap/NativeRuntimeLibraries.java +++ b/core/src/main/java/art/arcane/iris/platform/bootstrap/NativeRuntimeLibraries.java @@ -65,8 +65,7 @@ public final class NativeRuntimeLibraries { public URLClassLoader openProviderLoader(Path downloads) { try { List urls = providerUrls(downloads, NativeRuntimeLibraries.class.getClassLoader()); - URLClassLoader loader = new URLClassLoader("Iris native providers", urls.toArray(URL[]::new), - NativeAdapters.class.getClassLoader()); + URLClassLoader loader = providerLoader(urls.toArray(URL[]::new), NativeAdapters.class.getClassLoader()); NativeAdapters.registerProviderLoader(loader); return loader; } catch (IOException exception) { @@ -74,6 +73,10 @@ public final class NativeRuntimeLibraries { } } + static URLClassLoader providerLoader(URL[] urls, ClassLoader parent) { + return new ProviderClassLoader(urls, parent); + } + List providerUrls(Path downloads, ClassLoader resources) throws IOException { List urls = new ArrayList<>(providers.size()); for (Provider provider : providers) { @@ -123,4 +126,28 @@ public final class NativeRuntimeLibraries { private record Provider(String module, String version, String sha256) { } + + private static final class ProviderClassLoader extends URLClassLoader { + private ProviderClassLoader(URL[] urls, ClassLoader parent) { + super("Iris native providers", urls, parent); + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + synchronized (getClassLoadingLock(name)) { + Class loaded = findLoadedClass(name); + if (loaded == null) { + try { + loaded = findClass(name); + } catch (ClassNotFoundException absent) { + return super.loadClass(name, resolve); + } + } + if (resolve) { + resolveClass(loaded); + } + return loaded; + } + } + } } diff --git a/core/src/test/java/art/arcane/iris/platform/bootstrap/NativeRuntimeLibrariesTest.java b/core/src/test/java/art/arcane/iris/platform/bootstrap/NativeRuntimeLibrariesTest.java index 5ba346183..2f309242a 100644 --- a/core/src/test/java/art/arcane/iris/platform/bootstrap/NativeRuntimeLibrariesTest.java +++ b/core/src/test/java/art/arcane/iris/platform/bootstrap/NativeRuntimeLibrariesTest.java @@ -3,6 +3,8 @@ package art.arcane.iris.platform.bootstrap; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -25,6 +27,7 @@ import java.util.stream.Stream; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -41,6 +44,20 @@ public class NativeRuntimeLibrariesTest { assertThrows(IllegalStateException.class, () -> NativeRuntimeLibraries.from(fixture.manifest(), "26.2.0")); } + @Test + public void providerClassesLoadFromTheProviderJarBeforeThePluginLoader() throws Exception { + String implementation = "art/arcane/volmlib/nativelib/v26_2_R1/Probe"; + String api = "art/arcane/volmlib/nativelib/Api"; + Path plugin = classJar("plugin.jar", implementation, api); + Path provider = classJar("provider.jar", implementation); + try (URLClassLoader parent = new URLClassLoader(new URL[]{plugin.toUri().toURL()}, null); + URLClassLoader loader = NativeRuntimeLibraries.providerLoader(new URL[]{provider.toUri().toURL()}, parent)) { + assertSame(loader, loader.loadClass(implementation.replace('/', '.')).getClassLoader()); + assertSame(parent, loader.loadClass(api.replace('/', '.')).getClassLoader()); + assertSame(String.class, loader.loadClass("java.lang.String")); + } + } + @Test public void rejectsMinecraftVersionsWithoutProvider() throws Exception { EmbeddedFixture fixture = embeddedFixture(); @@ -156,6 +173,21 @@ public class NativeRuntimeLibrariesTest { return new EmbeddedFixture(properties, resources); } + private Path classJar(String name, String... classes) throws IOException { + Path jar = temporary.newFolder().toPath().resolve(name); + try (JarOutputStream output = new JarOutputStream(Files.newOutputStream(jar))) { + for (String type : classes) { + ClassWriter writer = new ClassWriter(0); + writer.visit(Opcodes.V17, Opcodes.ACC_PUBLIC, type, null, "java/lang/Object", null); + writer.visitEnd(); + output.putNextEntry(new JarEntry(type + ".class")); + output.write(writer.toByteArray()); + output.closeEntry(); + } + } + return jar; + } + private static void assertNoFailedExtract(Path downloads) throws IOException { try (Stream files = Files.walk(downloads)) { assertFalse(files.anyMatch(path -> Files.isRegularFile(path)