From 09c5ee46332e6a04f54787f1a74b17bcd1eab755 Mon Sep 17 00:00:00 2001 From: ldematte Date: Fri, 5 Sep 2025 09:49:21 +0200 Subject: [PATCH 1/5] Add reason to UnsupportedProvider/UnsupportedOperationExceptions --- .../nvidia/cuvs/spi/CuVSServiceProvider.java | 27 +++++++++------ .../nvidia/cuvs/spi/UnsupportedProvider.java | 34 +++++++++++-------- 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSServiceProvider.java b/java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSServiceProvider.java index c8c886dc13..a4a5843adf 100644 --- a/java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSServiceProvider.java +++ b/java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSServiceProvider.java @@ -17,6 +17,8 @@ import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; +import java.util.ArrayList; +import java.util.List; import java.util.ServiceLoader; /** @@ -43,7 +45,10 @@ private static CuVSProvider loadProvider() { } static CuVSProvider builtinProvider() { - if (Runtime.version().feature() > 21 && isLinuxAmd64()) { + var supportedJavaRuntime = Runtime.version().feature() > 21; + var supportedOs = System.getProperty("os.name").startsWith("Linux"); + var supportedArchitecture = System.getProperty("os.arch").equals("amd64"); + if (supportedJavaRuntime && supportedOs && supportedArchitecture) { try { var cls = Class.forName("com.nvidia.cuvs.spi.JDKProvider"); var ctr = MethodHandles.lookup().findConstructor(cls, MethodType.methodType(void.class)); @@ -52,16 +57,18 @@ static CuVSProvider builtinProvider() { throw new AssertionError(e); } } - return new UnsupportedProvider(); - } + List unsupportedReasons = new ArrayList<>(); + if (!supportedJavaRuntime) { + unsupportedReasons.add("cuvs-java requires Java Runtime version 22 or greater"); + } + if (!supportedOs) { + unsupportedReasons.add("cuvs-java supports only Linux"); + } + if (!supportedArchitecture) { + unsupportedReasons.add("cuvs-java supports only x86"); + } - /** - * Returns true iff the architecture is x64 (amd64) and the OS Linux - * (the * OS we currently support for the native lib). - */ - static boolean isLinuxAmd64() { - String name = System.getProperty("os.name"); - return (name.startsWith("Linux")) && System.getProperty("os.arch").equals("amd64"); + return new UnsupportedProvider(String.join("; ", unsupportedReasons)); } } } diff --git a/java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/UnsupportedProvider.java b/java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/UnsupportedProvider.java index 60509004c4..0f1edda99c 100644 --- a/java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/UnsupportedProvider.java +++ b/java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/UnsupportedProvider.java @@ -24,51 +24,57 @@ */ final class UnsupportedProvider implements CuVSProvider { + private final String reasons; + + public UnsupportedProvider(String reasons) { + this.reasons = reasons; + } + @Override public CuVSResources newCuVSResources(Path tempDirectory) { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException(reasons); } @Override public BruteForceIndex.Builder newBruteForceIndexBuilder(CuVSResources cuVSResources) { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException(reasons); } @Override public CagraIndex.Builder newCagraIndexBuilder(CuVSResources cuVSResources) { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException(reasons); } @Override public HnswIndex.Builder newHnswIndexBuilder(CuVSResources cuVSResources) { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException(reasons); } @Override public TieredIndex.Builder newTieredIndexBuilder(CuVSResources cuVSResources) { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException(reasons); } @Override public CagraIndex mergeCagraIndexes(CagraIndex[] indexes) { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException(reasons); } @Override public CuVSMatrix.Builder newHostMatrixBuilder( long size, long dimensions, CuVSMatrix.DataType dataType) { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException(reasons); } @Override public CuVSMatrix.Builder newDeviceMatrixBuilder( CuVSResources cuVSResources, long size, long dimensions, CuVSMatrix.DataType dataType) { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException(reasons); } @Override public GPUInfoProvider gpuInfoProvider() { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException(reasons); } @Override @@ -79,26 +85,26 @@ public CuVSMatrix.Builder newDeviceMatrixBuilder( int rowStride, int columnStride, CuVSMatrix.DataType dataType) { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException(reasons); } @Override public MethodHandle newNativeMatrixBuilder() { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException(reasons); } @Override public CuVSMatrix newMatrixFromArray(float[][] vectors) { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException(reasons); } @Override public CuVSMatrix newMatrixFromArray(int[][] vectors) { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException(reasons); } @Override public CuVSMatrix newMatrixFromArray(byte[][] vectors) { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException(reasons); } } From b6810e37015fad89310bb3594100ef0f027db17c Mon Sep 17 00:00:00 2001 From: ldematte Date: Fri, 5 Sep 2025 12:30:50 +0200 Subject: [PATCH 2/5] Add cuvs version check to JDKProvider --- java/cuvs-java/pom.xml | 17 +++++++ .../nvidia/cuvs/spi/CuVSServiceProvider.java | 6 ++- .../spi/ProviderInitializationException.java | 26 +++++++++++ .../com/nvidia/cuvs/spi/JDKProvider.java | 44 +++++++++++++++++++ 4 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/ProviderInitializationException.java diff --git a/java/cuvs-java/pom.xml b/java/cuvs-java/pom.xml index b3b862d291..07c364ee03 100644 --- a/java/cuvs-java/pom.xml +++ b/java/cuvs-java/pom.xml @@ -62,6 +62,7 @@ 22 UTF-8 UTF-8 + ${project.version} @@ -155,6 +156,22 @@ + + org.codehaus.mojo + properties-maven-plugin + 1.0.0 + + + generate-resources + + write-project-properties + + + ${project.build.outputDirectory}/version.properties + + + + org.apache.maven.plugins maven-assembly-plugin diff --git a/java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSServiceProvider.java b/java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSServiceProvider.java index a4a5843adf..ae4c9b083a 100644 --- a/java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSServiceProvider.java +++ b/java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSServiceProvider.java @@ -51,8 +51,12 @@ static CuVSProvider builtinProvider() { if (supportedJavaRuntime && supportedOs && supportedArchitecture) { try { var cls = Class.forName("com.nvidia.cuvs.spi.JDKProvider"); - var ctr = MethodHandles.lookup().findConstructor(cls, MethodType.methodType(void.class)); + var ctr = + MethodHandles.lookup() + .findStatic(cls, "create", MethodType.methodType(CuVSProvider.class)); return (CuVSProvider) ctr.invoke(); + } catch (ProviderInitializationException e) { + return new UnsupportedProvider("cannot create JDKProvider: " + e.getMessage()); } catch (Throwable e) { throw new AssertionError(e); } diff --git a/java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/ProviderInitializationException.java b/java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/ProviderInitializationException.java new file mode 100644 index 0000000000..1a6c34a2fd --- /dev/null +++ b/java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/ProviderInitializationException.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2025, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.cuvs.spi; + +class ProviderInitializationException extends Exception { + ProviderInitializationException(String message, Throwable cause) { + super(message, cause); + } + + public ProviderInitializationException(String message) { + super(message); + } +} diff --git a/java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/JDKProvider.java b/java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/JDKProvider.java index 0ee0cf1d10..a9128193a9 100644 --- a/java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/JDKProvider.java +++ b/java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/JDKProvider.java @@ -16,10 +16,14 @@ package com.nvidia.cuvs.spi; import static com.nvidia.cuvs.internal.common.Util.*; +import static com.nvidia.cuvs.internal.panama.headers_h.cuvsVersionGet; +import static com.nvidia.cuvs.internal.panama.headers_h.uint16_t; import com.nvidia.cuvs.*; import com.nvidia.cuvs.internal.*; import com.nvidia.cuvs.internal.common.Util; +import java.io.IOException; +import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; @@ -28,11 +32,51 @@ import java.nio.file.Path; import java.util.Locale; import java.util.Objects; +import java.util.Properties; final class JDKProvider implements CuVSProvider { private static final MethodHandle createNativeDataset$mh = createNativeDatasetBuilder(); + static CuVSProvider create() throws Throwable { + var mavenVersion = readMavenVersionFromPropertiesOrNull(); + + try (var localArena = Arena.ofConfined()) { + var majorPtr = localArena.allocate(uint16_t); + var minorPtr = localArena.allocate(uint16_t); + var patchPtr = localArena.allocate(uint16_t); + checkCuVSError(cuvsVersionGet(majorPtr, minorPtr, patchPtr), "cuvsVersionGet"); + var major = majorPtr.get(uint16_t, 0); + var minor = minorPtr.get(uint16_t, 0); + var patch = patchPtr.get(uint16_t, 0); + + var cuvsVersionString = String.format(Locale.ROOT, "%02d.%02d.%d", major, minor, patch); + if (mavenVersion != null && !cuvsVersionString.equals(mavenVersion)) { + throw new ProviderInitializationException( + String.format( + Locale.ROOT, + "libcuvs_c version mismatch: expected [%s], found [%s]", + mavenVersion, + cuvsVersionString)); + } + } + return new JDKProvider(); + } + + /** + * Read cuvs-java version from Maven generated properties, or null if these are not available + */ + private static String readMavenVersionFromPropertiesOrNull() { + var properties = new Properties(); + + try (var is = JDKProvider.class.getClassLoader().getResourceAsStream("version.properties")) { + properties.load(is); + return properties.getProperty("version"); + } catch (IOException e) { + return null; + } + } + static MethodHandle createNativeDatasetBuilder() { try { var lookup = MethodHandles.lookup(); From 56561079a8e11ea1d77e88092e903c690097636b Mon Sep 17 00:00:00 2001 From: ldematte Date: Fri, 5 Sep 2025 12:46:47 +0200 Subject: [PATCH 3/5] Add cuvs library load check to JDKProvider --- java/cuvs-java/pom.xml | 2 +- .../internal/common/NativeLibraryUtils.java | 43 +++++++++++++++++++ .../com/nvidia/cuvs/spi/JDKProvider.java | 28 ++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/common/NativeLibraryUtils.java diff --git a/java/cuvs-java/pom.xml b/java/cuvs-java/pom.xml index 07c364ee03..e11f2ea0c0 100644 --- a/java/cuvs-java/pom.xml +++ b/java/cuvs-java/pom.xml @@ -159,7 +159,7 @@ org.codehaus.mojo properties-maven-plugin - 1.0.0 + 1.2.1 generate-resources diff --git a/java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/common/NativeLibraryUtils.java b/java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/common/NativeLibraryUtils.java new file mode 100644 index 0000000000..b09d528224 --- /dev/null +++ b/java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/common/NativeLibraryUtils.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.cuvs.internal.common; + +import java.lang.foreign.*; +import java.lang.invoke.MethodHandle; + +public class NativeLibraryUtils { + + private NativeLibraryUtils() {} + + private static final SymbolLookup LOOKUP = + SymbolLookup.libraryLookup(System.mapLibraryName("jvm"), Arena.ofAuto()) + .or(SymbolLookup.loaderLookup()) + .or(Linker.nativeLinker().defaultLookup()); + + // void * JVM_LoadLibrary(const char *name, jboolean throwException); + public static MethodHandle JVM_LoadLibrary$mh = + Linker.nativeLinker() + .downcallHandle( + LOOKUP.find("JVM_LoadLibrary").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_BOOLEAN)); + // void JVM_UnloadLibrary(void * handle); + public static MethodHandle JVM_UnloadLibrary$mh = + Linker.nativeLinker() + .downcallHandle( + LOOKUP.find("JVM_UnloadLibrary").orElseThrow(), + FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)); +} diff --git a/java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/JDKProvider.java b/java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/JDKProvider.java index a9128193a9..9b5a490afb 100644 --- a/java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/JDKProvider.java +++ b/java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/JDKProvider.java @@ -15,6 +15,8 @@ */ package com.nvidia.cuvs.spi; +import static com.nvidia.cuvs.internal.common.NativeLibraryUtils.JVM_LoadLibrary$mh; +import static com.nvidia.cuvs.internal.common.NativeLibraryUtils.JVM_UnloadLibrary$mh; import static com.nvidia.cuvs.internal.common.Util.*; import static com.nvidia.cuvs.internal.panama.headers_h.cuvsVersionGet; import static com.nvidia.cuvs.internal.panama.headers_h.uint16_t; @@ -59,6 +61,32 @@ static CuVSProvider create() throws Throwable { mavenVersion, cuvsVersionString)); } + } catch (ExceptionInInitializerError e) { + if (e.getCause() instanceof IllegalArgumentException) { + // Try to find if we failed to load libcuvs and why + // jextract loads the dynamic library with SymbolLookup.libraryLookup; this uses + // RawNativeLibraries::load + // https://github.com/openjdk/jdk/blob/master/src/java.base/share/native/libjava/RawNativeLibraries.c#L58 + // RawNativeLibraries::load in turn calls JVM_LoadLibrary. Unfortunately, it calls it with a + // JNI_FALSE parameter for throwException, which means that the detailed error messages are + // not surfaced. + // Here we try and load it again, with throwException true, so we can see what's broken + try (var localArena = Arena.ofConfined()) { + var name = localArena.allocateFrom(System.mapLibraryName("cuvs_c")); + Object lib = JVM_LoadLibrary$mh.invoke(name, true); + if (lib != null) { + // It wasn't a problem with library loading, so undo what we did + JVM_UnloadLibrary$mh.invoke(lib); + } + } catch (Throwable ex) { + if (ex instanceof UnsatisfiedLinkError ulex) { + throw new ProviderInitializationException(ulex.getMessage(), ulex); + } + throw new ProviderInitializationException("error while loading libcuvs", ex); + } + } else { + throw e.getCause() != null ? e.getCause() : e; + } } return new JDKProvider(); } From fc8404254541166168e4c147fa62910a63995b32 Mon Sep 17 00:00:00 2001 From: ldematte Date: Fri, 12 Sep 2025 12:17:38 +0200 Subject: [PATCH 4/5] Extend NativeDependencyLoader to use different strategies for fat-jar vs slim-jar --- java/cuvs-java/pom.xml | 4 +- .../com/nvidia/cuvs/spi/JDKProvider.java | 78 +------- .../cuvs/spi/NativeDependencyLoader.java | 167 ++++++++++++++++++ .../spi/OptionalNativeDependencyLoader.java | 81 --------- 4 files changed, 175 insertions(+), 155 deletions(-) create mode 100644 java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/NativeDependencyLoader.java delete mode 100644 java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/OptionalNativeDependencyLoader.java diff --git a/java/cuvs-java/pom.xml b/java/cuvs-java/pom.xml index ca27c67635..bc341299b1 100644 --- a/java/cuvs-java/pom.xml +++ b/java/cuvs-java/pom.xml @@ -326,7 +326,7 @@ true true - com.nvidia.cuvs.examples.CagraExample + 12 @@ -409,7 +409,7 @@ true true - com.nvidia.cuvs.examples.CagraExample + 13 diff --git a/java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/JDKProvider.java b/java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/JDKProvider.java index 9b23003cdb..5ea9519949 100644 --- a/java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/JDKProvider.java +++ b/java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/JDKProvider.java @@ -15,17 +15,11 @@ */ package com.nvidia.cuvs.spi; -import static com.nvidia.cuvs.internal.common.NativeLibraryUtils.JVM_LoadLibrary$mh; -import static com.nvidia.cuvs.internal.common.NativeLibraryUtils.JVM_UnloadLibrary$mh; import static com.nvidia.cuvs.internal.common.Util.*; -import static com.nvidia.cuvs.internal.panama.headers_h.cuvsVersionGet; -import static com.nvidia.cuvs.internal.panama.headers_h.uint16_t; import com.nvidia.cuvs.*; import com.nvidia.cuvs.internal.*; import com.nvidia.cuvs.internal.common.Util; -import java.io.IOException; -import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; @@ -34,79 +28,19 @@ import java.nio.file.Path; import java.util.Locale; import java.util.Objects; -import java.util.Properties; final class JDKProvider implements CuVSProvider { - static { - OptionalNativeDependencyLoader.loadLibraries(); - } - private static final MethodHandle createNativeDataset$mh = createNativeDatasetBuilder(); + private JDKProvider() {} + static CuVSProvider create() throws Throwable { - var mavenVersion = readMavenVersionFromPropertiesOrNull(); - - try (var localArena = Arena.ofConfined()) { - var majorPtr = localArena.allocate(uint16_t); - var minorPtr = localArena.allocate(uint16_t); - var patchPtr = localArena.allocate(uint16_t); - checkCuVSError(cuvsVersionGet(majorPtr, minorPtr, patchPtr), "cuvsVersionGet"); - var major = majorPtr.get(uint16_t, 0); - var minor = minorPtr.get(uint16_t, 0); - var patch = patchPtr.get(uint16_t, 0); - - var cuvsVersionString = String.format(Locale.ROOT, "%02d.%02d.%d", major, minor, patch); - if (mavenVersion != null && !cuvsVersionString.equals(mavenVersion)) { - throw new ProviderInitializationException( - String.format( - Locale.ROOT, - "libcuvs_c version mismatch: expected [%s], found [%s]", - mavenVersion, - cuvsVersionString)); - } - } catch (ExceptionInInitializerError e) { - if (e.getCause() instanceof IllegalArgumentException) { - // Try to find if we failed to load libcuvs and why - // jextract loads the dynamic library with SymbolLookup.libraryLookup; this uses - // RawNativeLibraries::load - // https://github.com/openjdk/jdk/blob/master/src/java.base/share/native/libjava/RawNativeLibraries.c#L58 - // RawNativeLibraries::load in turn calls JVM_LoadLibrary. Unfortunately, it calls it with a - // JNI_FALSE parameter for throwException, which means that the detailed error messages are - // not surfaced. - // Here we try and load it again, with throwException true, so we can see what's broken - try (var localArena = Arena.ofConfined()) { - var name = localArena.allocateFrom(System.mapLibraryName("cuvs_c")); - Object lib = JVM_LoadLibrary$mh.invoke(name, true); - if (lib != null) { - // It wasn't a problem with library loading, so undo what we did - JVM_UnloadLibrary$mh.invoke(lib); - } - } catch (Throwable ex) { - if (ex instanceof UnsatisfiedLinkError ulex) { - throw new ProviderInitializationException(ulex.getMessage(), ulex); - } - throw new ProviderInitializationException("error while loading libcuvs", ex); - } - } else { - throw e.getCause() != null ? e.getCause() : e; - } - } - return new JDKProvider(); - } + NativeDependencyLoader.loadLibraries(); - /** - * Read cuvs-java version from Maven generated properties, or null if these are not available - */ - private static String readMavenVersionFromPropertiesOrNull() { - var properties = new Properties(); - - try (var is = JDKProvider.class.getClassLoader().getResourceAsStream("version.properties")) { - properties.load(is); - return properties.getProperty("version"); - } catch (IOException e) { - return null; - } + // TODO: version check from #1315 + + return new JDKProvider(); } static MethodHandle createNativeDatasetBuilder() { diff --git a/java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/NativeDependencyLoader.java b/java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/NativeDependencyLoader.java new file mode 100644 index 0000000000..31c80b5628 --- /dev/null +++ b/java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/NativeDependencyLoader.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2025, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nvidia.cuvs.spi; + +import static com.nvidia.cuvs.internal.common.NativeLibraryUtils.JVM_LoadLibrary$mh; + +import java.io.*; +import java.lang.foreign.Arena; +import java.net.URL; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +/** + * A class that loads native dependencies if they are available in the jar. + */ +public class NativeDependencyLoader { + + interface NativeDependencyLoaderStrategy { + void loadLibraries(); + } + + private static final NativeDependencyLoaderStrategy LOADER_STRATEGY = createLoaderStrategy(); + + private static NativeDependencyLoaderStrategy createLoaderStrategy() { + if (jarHasNativeDependencies()) { + return new EmbeddedNativeDependencyLoaderStrategy(); + } else { + return new SystemNativeDependencyLoaderStrategy(); + } + } + + private static boolean jarHasNativeDependencies() { + try (var jarFile = + new JarFile( + JDKProvider.class.getProtectionDomain().getCodeSource().getLocation().getPath())) { + Manifest manifest = jarFile.getManifest(); + // TODO: use this to add a check on the installed CUDA version (which will be system-loaded in + // any case) + var embeddedLibrariesCudaVersion = + manifest.getMainAttributes().getValue("Embedded-Libraries-Cuda-Version"); + return embeddedLibrariesCudaVersion != null; + } catch (IOException e) { + return false; + } + } + + private static boolean loaded = false; + + public static void loadLibraries() { + if (!loaded) { + try { + LOADER_STRATEGY.loadLibraries(); + } finally { + loaded = true; + } + } + } + + private static class EmbeddedNativeDependencyLoaderStrategy + implements NativeDependencyLoaderStrategy { + + private static final String OS = System.getProperty("os.name"); + private static final String ARCH = System.getProperty("os.arch"); + private static final ClassLoader CLASS_LOADER = JDKProvider.class.getClassLoader(); + + private static final String[] FILES_TO_LOAD = { + "rapids_logger", "rmm", "cuvs", "cuvs_c", + }; + + @Override + public void loadLibraries() { + for (String file : FILES_TO_LOAD) { + // Uncomment the following line to trace the loading of native dependencies. + // System.out.println("Loading native dependency: " + file); + try { + System.load(createFile(file).getAbsolutePath()); + } catch (Throwable t) { + var ex = + new UnsatisfiedLinkError( + "Failed to load native dependency: " + + System.mapLibraryName(file) + + ".so: " + + t.getMessage()); + ex.initCause(t); + throw ex; + } + } + } + + /** + * Extract the contents of a library resource into a temporary file + */ + private static File createFile(String baseName) throws IOException { + String path = + EmbeddedNativeDependencyLoaderStrategy.ARCH + + "/" + + EmbeddedNativeDependencyLoaderStrategy.OS + + "/" + + System.mapLibraryName(baseName); + File loc; + URL resource = CLASS_LOADER.getResource(path); + if (resource == null) { + throw new FileNotFoundException("Could not locate native dependency " + path); + } + try (InputStream in = resource.openStream()) { + loc = File.createTempFile(baseName, ".so"); + loc.deleteOnExit(); + try (OutputStream out = new FileOutputStream(loc)) { + byte[] buffer = new byte[1024 * 16]; + int read = 0; + while ((read = in.read(buffer)) >= 0) { + out.write(buffer, 0, read); + } + } + } + return loc; + } + } + + private static class SystemNativeDependencyLoaderStrategy + implements NativeDependencyLoaderStrategy { + + @Override + public void loadLibraries() { + // Try load libcuvs using directly JVM_LoadLibrary with the correct flags for in-depth failure + // diagnosis. + // + // jextract loads the dynamic libraries it references with SymbolLookup.libraryLookup; this + // uses + // RawNativeLibraries::load + // https://github.com/openjdk/jdk/blob/master/src/java.base/share/native/libjava/RawNativeLibraries.c#L58 + // RawNativeLibraries::load in turn calls JVM_LoadLibrary. Unfortunately, it calls it with a + // JNI_FALSE parameter for throwException, which means that the detailed error messages are + // not surfaced. + // + // Here we invoke it with throwException true, so in case of error we can see what's broken + try (var localArena = Arena.ofConfined()) { + var name = localArena.allocateFrom(System.mapLibraryName("cuvs_c")); + Object lib = JVM_LoadLibrary$mh.invoke(name, true); + if (lib == null) { + throw new UnsatisfiedLinkError("Unspecified failure loading libcuvs"); + } + } catch (Throwable ex) { + if (ex instanceof UnsatisfiedLinkError ulex) { + throw ulex; // new ProviderInitializationException(ulex.getMessage(), ulex); + } + // throw new ProviderInitializationException("error while loading libcuvs", ex); + var ulex = new UnsatisfiedLinkError("Unspecified failure loading libcuvs"); + ulex.initCause(ex); + throw ulex; + } + } + } +} diff --git a/java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/OptionalNativeDependencyLoader.java b/java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/OptionalNativeDependencyLoader.java deleted file mode 100644 index facbb670a3..0000000000 --- a/java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/OptionalNativeDependencyLoader.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2025, NVIDIA CORPORATION. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.nvidia.cuvs.spi; - -import java.io.*; -import java.net.URL; -import java.util.stream.*; - -/** - * A class that loads native dependencies if they are available in the jar. - */ -public class OptionalNativeDependencyLoader { - - private static final ClassLoader loader = JDKProvider.class.getClassLoader(); - - private static boolean loaded = false; - - private static final String[] FILES_TO_LOAD = { - "rapids_logger", "rmm", "cuvs", "cuvs_c", - }; - - public static void loadLibraries() { - if (!loaded) { - String os = System.getProperty("os.name"); - String arch = System.getProperty("os.arch"); - - Stream.of(FILES_TO_LOAD) - .forEach( - file -> { - // Uncomment the following line to trace the loading of native dependencies. - // System.out.println("Loading native dependency: " + file); - try { - System.load(createFile(os, arch, file).getAbsolutePath()); - } catch (Throwable t) { - System.err.println( - "Continuing despite failure to load native dependency: " - + System.mapLibraryName(file) - + ".so: " - + t.getMessage()); - } - }); - - loaded = true; - } - } - - /** Extract the contents of a library resource into a temporary file */ - private static File createFile(String os, String arch, String baseName) throws IOException { - String path = arch + "/" + os + "/" + System.mapLibraryName(baseName); - File loc; - URL resource = loader.getResource(path); - if (resource == null) { - throw new FileNotFoundException("Could not locate native dependency " + path); - } - try (InputStream in = resource.openStream()) { - loc = File.createTempFile(baseName, ".so"); - loc.deleteOnExit(); - try (OutputStream out = new FileOutputStream(loc)) { - byte[] buffer = new byte[1024 * 16]; - int read = 0; - while ((read = in.read(buffer)) >= 0) { - out.write(buffer, 0, read); - } - } - } - return loc; - } -} From 9efc1ea31fa8b1f4e92eee3b5c8f1fe9e7ae7a65 Mon Sep 17 00:00:00 2001 From: ldematte Date: Sun, 14 Sep 2025 09:27:33 +0200 Subject: [PATCH 5/5] Use ProviderInitializationException for loadLibrary failures --- .../nvidia/cuvs/spi/CuVSServiceProvider.java | 2 +- .../com/nvidia/cuvs/spi/JDKProvider.java | 2 +- .../cuvs/spi/NativeDependencyLoader.java | 60 +++++++++---------- 3 files changed, 30 insertions(+), 34 deletions(-) diff --git a/java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSServiceProvider.java b/java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSServiceProvider.java index ae4c9b083a..56db88098d 100644 --- a/java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSServiceProvider.java +++ b/java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSServiceProvider.java @@ -56,7 +56,7 @@ static CuVSProvider builtinProvider() { .findStatic(cls, "create", MethodType.methodType(CuVSProvider.class)); return (CuVSProvider) ctr.invoke(); } catch (ProviderInitializationException e) { - return new UnsupportedProvider("cannot create JDKProvider: " + e.getMessage()); + return new UnsupportedProvider("Cannot create JDKProvider: " + e.getMessage()); } catch (Throwable e) { throw new AssertionError(e); } diff --git a/java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/JDKProvider.java b/java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/JDKProvider.java index 25ba2a0a72..6f3d3fa6e3 100644 --- a/java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/JDKProvider.java +++ b/java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/JDKProvider.java @@ -41,7 +41,7 @@ final class JDKProvider implements CuVSProvider { private JDKProvider() {} - static CuVSProvider create() throws Throwable { + static CuVSProvider create() throws ProviderInitializationException { NativeDependencyLoader.loadLibraries(); var mavenVersion = readCuVSVersionFromManifest(); diff --git a/java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/NativeDependencyLoader.java b/java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/NativeDependencyLoader.java index 31c80b5628..436008ed52 100644 --- a/java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/NativeDependencyLoader.java +++ b/java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/NativeDependencyLoader.java @@ -20,16 +20,18 @@ import java.io.*; import java.lang.foreign.Arena; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.util.jar.JarFile; import java.util.jar.Manifest; /** * A class that loads native dependencies if they are available in the jar. */ -public class NativeDependencyLoader { +class NativeDependencyLoader { interface NativeDependencyLoaderStrategy { - void loadLibraries(); + void loadLibraries() throws ProviderInitializationException; } private static final NativeDependencyLoaderStrategy LOADER_STRATEGY = createLoaderStrategy(); @@ -47,8 +49,8 @@ private static boolean jarHasNativeDependencies() { new JarFile( JDKProvider.class.getProtectionDomain().getCodeSource().getLocation().getPath())) { Manifest manifest = jarFile.getManifest(); - // TODO: use this to add a check on the installed CUDA version (which will be system-loaded in - // any case) + // TODO: use this variable to add a check on the installed CUDA version + // (which will be system-loaded in any case, even with the fat-jar) var embeddedLibrariesCudaVersion = manifest.getMainAttributes().getValue("Embedded-Libraries-Cuda-Version"); return embeddedLibrariesCudaVersion != null; @@ -59,7 +61,7 @@ private static boolean jarHasNativeDependencies() { private static boolean loaded = false; - public static void loadLibraries() { + static void loadLibraries() throws ProviderInitializationException { if (!loaded) { try { LOADER_STRATEGY.loadLibraries(); @@ -81,21 +83,19 @@ private static class EmbeddedNativeDependencyLoaderStrategy }; @Override - public void loadLibraries() { + public void loadLibraries() throws ProviderInitializationException { for (String file : FILES_TO_LOAD) { // Uncomment the following line to trace the loading of native dependencies. // System.out.println("Loading native dependency: " + file); try { System.load(createFile(file).getAbsolutePath()); } catch (Throwable t) { - var ex = - new UnsatisfiedLinkError( - "Failed to load native dependency: " - + System.mapLibraryName(file) - + ".so: " - + t.getMessage()); - ex.initCause(t); - throw ex; + throw new ProviderInitializationException( + "Failed to load native dependency: " + + System.mapLibraryName(file) + + ".so: " + + t.getMessage(), + t); } } } @@ -118,13 +118,8 @@ private static File createFile(String baseName) throws IOException { try (InputStream in = resource.openStream()) { loc = File.createTempFile(baseName, ".so"); loc.deleteOnExit(); - try (OutputStream out = new FileOutputStream(loc)) { - byte[] buffer = new byte[1024 * 16]; - int read = 0; - while ((read = in.read(buffer)) >= 0) { - out.write(buffer, 0, read); - } - } + + Files.copy(in, loc.toPath(), StandardCopyOption.REPLACE_EXISTING); } return loc; } @@ -134,7 +129,7 @@ private static class SystemNativeDependencyLoaderStrategy implements NativeDependencyLoaderStrategy { @Override - public void loadLibraries() { + public void loadLibraries() throws ProviderInitializationException { // Try load libcuvs using directly JVM_LoadLibrary with the correct flags for in-depth failure // diagnosis. // @@ -147,20 +142,21 @@ public void loadLibraries() { // not surfaced. // // Here we invoke it with throwException true, so in case of error we can see what's broken + String cuvsLibraryName = System.mapLibraryName("cuvs_c"); + + final Object lib; try (var localArena = Arena.ofConfined()) { - var name = localArena.allocateFrom(System.mapLibraryName("cuvs_c")); - Object lib = JVM_LoadLibrary$mh.invoke(name, true); - if (lib == null) { - throw new UnsatisfiedLinkError("Unspecified failure loading libcuvs"); - } + var name = localArena.allocateFrom(cuvsLibraryName); + lib = JVM_LoadLibrary$mh.invoke(name, true); } catch (Throwable ex) { if (ex instanceof UnsatisfiedLinkError ulex) { - throw ulex; // new ProviderInitializationException(ulex.getMessage(), ulex); + throw new ProviderInitializationException(ulex.getMessage(), ulex); + } else { + throw new ProviderInitializationException("Error while loading " + cuvsLibraryName, ex); } - // throw new ProviderInitializationException("error while loading libcuvs", ex); - var ulex = new UnsatisfiedLinkError("Unspecified failure loading libcuvs"); - ulex.initCause(ex); - throw ulex; + } + if (lib == null) { + throw new ProviderInitializationException("Unspecified failure loading " + cuvsLibraryName); } } }