Skip to content

Commit f0cbf1a

Browse files
authored
Native libraries from packages are not copied (#13101)
Removes the copying of native libraries next to the generated NI binary. Native libraries of packages are loaded from the same location in both JVM and AOT modes. **Old behavior:** [EnsoLibraryFeature](https://github.com/enso-org/enso/blob/7106fdd1cc32902554e7318e811d66e66cc6f3e4/engine/runner/src/main/java/org/enso/runner/EnsoLibraryFeature.java) copies all the native libraries within `**/polyglot/lib/**` next to the generated NI binary. The motivation for that is mentioned in the description of #11874. **New behavior:** [EnsoLibraryFeature](https://github.com/enso-org/enso/blob/7106fdd1cc32902554e7318e811d66e66cc6f3e4/engine/runner/src/main/java/org/enso/runner/EnsoLibraryFeature.java) no longer copies native libraries. Instead, once a package is loaded in [DefaultPackageRepository.registerPackageInternal](https://github.com/enso-org/enso/blob/d369fcbe69f3eb1baa5fe7a5b8d24354b74067ad/engine/runtime/src/main/scala/org/enso/interpreter/runtime/DefaultPackageRepository.scala#L203-L247), paths to its native libraries are added to the native library search path via [NativeLibrarySearchPath](https://github.com/enso-org/enso/blob/9d8866944b145add67bbb944328ded1e3c4a3bba/engine/runtime/src/main/java/org/enso/interpreter/runtime/nativeimage/NativeLibrarySearchPath.java). Inspired by https://github.com/Akirathan/native-lib-loader. # Important Notes - In JVM mode, native libraries of packages are still loaded by [HostClassLoader.findLibrary](https://github.com/enso-org/enso/blob/7658faf64563ef25e8f04003892cfb074fd907c5/engine/runtime/src/main/java/org/enso/interpreter/runtime/HostClassLoader.java#L86-L109). - In AOT mode, [com.oracle.svm.core.jdk.NativeLibraries#usrPaths](https://github.com/oracle/graal/blob/91061cb6a151ffaa83e05f6852384537c2c85e6b/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/jdk/NativeLibraries.java#L67) field is changed at runtime.
1 parent 06e7875 commit f0cbf1a

File tree

6 files changed

+168
-37
lines changed

6 files changed

+168
-37
lines changed

build.sbt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4052,8 +4052,10 @@ lazy val `engine-runner` = project
40524052
// Workaround a problem with build-/runtime-initialization conflict
40534053
// by disabling this service provider
40544054
"-H:ServiceLoaderFeatureExcludeServiceProviders=net.snowflake.client.core.FileTypeDetector",
4055-
"-Dorg.enso.feature.native.lib.output=" + (engineDistributionRoot.value / "bin"),
40564055
"-Dorg.sqlite.lib.exportPath=" + (engineDistributionRoot.value / "bin"),
4056+
"--features=org.enso.interpreter.runtime.nativeimage.NativeLibraryFeature",
4057+
// Needed for the NativeLibraryFeature
4058+
"--add-opens=org.graalvm.nativeimage.builder/com.oracle.svm.core.jdk=ALL-UNNAMED",
40574059
// Snowflake uses Apache Arrow (equivalent of #9664 in native-image setup)
40584060
"--add-opens=java.base/java.nio=ALL-UNNAMED"
40594061
) ++ (if (GraalVM.EnsoLauncher.debug) {
@@ -4109,7 +4111,8 @@ lazy val `engine-runner` = project
41094111
initializeAtBuildtime = NativeImage.defaultBuildTimeInitClasses ++
41104112
Seq(
41114113
"org.bouncycastle",
4112-
"org.enso.snowflake.BouncyCastleInitializer"
4114+
"org.enso.snowflake.BouncyCastleInitializer",
4115+
"org.enso.interpreter.runtime.nativeimage"
41134116
)
41144117
)
41154118
}

engine/runner/src/main/java/org/enso/runner/EnsoLibraryFeature.java

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,18 @@
22

33
import static scala.jdk.javaapi.CollectionConverters.asJava;
44

5-
import java.io.File;
65
import java.nio.file.Files;
76
import java.nio.file.Path;
8-
import java.nio.file.StandardCopyOption;
97
import java.util.LinkedHashSet;
108
import java.util.TreeSet;
119
import org.enso.compiler.core.EnsoParser;
1210
import org.enso.compiler.core.ir.module.scope.imports.Polyglot;
13-
import org.enso.filesystem.FileSystem$;
14-
import org.enso.pkg.NativeLibraryFinder;
1511
import org.enso.pkg.PackageManager$;
1612
import org.graalvm.nativeimage.hosted.Feature;
1713
import org.graalvm.nativeimage.hosted.RuntimeProxyCreation;
1814
import org.graalvm.nativeimage.hosted.RuntimeReflection;
1915

2016
public final class EnsoLibraryFeature implements Feature {
21-
private static final String LIB_OUTPUT = "org.enso.feature.native.lib.output";
22-
private final File nativeLibDir;
23-
24-
public EnsoLibraryFeature() {
25-
var nativeLibOut = System.getProperty(LIB_OUTPUT);
26-
if (nativeLibOut == null) {
27-
throw new IllegalStateException("Missing system property: " + LIB_OUTPUT);
28-
}
29-
nativeLibDir = new File(nativeLibOut);
30-
if (!nativeLibDir.exists() || !nativeLibDir.isDirectory()) {
31-
var created = nativeLibDir.mkdirs();
32-
if (!created) {
33-
throw new IllegalStateException("Cannot create directory: " + nativeLibDir);
34-
}
35-
}
36-
}
37-
3817
@Override
3918
public void beforeAnalysis(BeforeAnalysisAccess access) {
4019

@@ -68,7 +47,6 @@ public void beforeAnalysis(BeforeAnalysisAccess access) {
6847
*/
6948

7049
var classes = new TreeSet<String>();
71-
var nativeLibPaths = new TreeSet<String>();
7250
try {
7351
for (var p : libs) {
7452
var result = PackageManager$.MODULE$.Default().loadPackage(p.toFile());
@@ -106,15 +84,6 @@ public void beforeAnalysis(BeforeAnalysisAccess access) {
10684
}
10785
}
10886
}
109-
if (pkg.nativeLibraryDir().exists()) {
110-
var nativeLibs =
111-
NativeLibraryFinder.listAllNativeLibraries(pkg, FileSystem$.MODULE$.defaultFs());
112-
for (var nativeLib : nativeLibs) {
113-
var out = new File(nativeLibDir, nativeLib.getName());
114-
Files.copy(nativeLib.toPath(), out.toPath(), StandardCopyOption.REPLACE_EXISTING);
115-
nativeLibPaths.add(out.getAbsolutePath());
116-
}
117-
}
11887
pkg.markAotReady();
11988
}
12089
}
@@ -127,6 +96,5 @@ public void beforeAnalysis(BeforeAnalysisAccess access) {
12796
System.err.println(" " + className);
12897
}
12998
System.err.println("Registered " + classes.size() + " classes for reflection");
130-
System.err.println("Copied native libraries: " + nativeLibPaths + " into " + nativeLibDir);
13199
}
132100
}

engine/runtime/src/main/java/module-info.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
requires org.apache.tika.core;
2929
requires org.slf4j;
3030
requires org.graalvm.collections;
31+
requires org.graalvm.nativeimage;
3132
requires org.graalvm.polyglot;
3233
requires org.graalvm.truffle;
3334
requires com.ibm.icu;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package org.enso.interpreter.runtime.nativeimage;
2+
3+
import java.lang.invoke.MethodHandle;
4+
import java.lang.invoke.MethodHandles;
5+
import java.lang.reflect.Field;
6+
import org.graalvm.nativeimage.ImageSingletons;
7+
import org.graalvm.nativeimage.hosted.Feature;
8+
9+
/** This feature sets the static fields of {@link NativeLibrarySearchPath} during NI build time. */
10+
final class NativeLibraryFeature implements Feature {
11+
private static final String NATIVE_LIBS_CLASS_NAME = "com.oracle.svm.core.jdk.NativeLibraries";
12+
private static final String NATIVE_LIBS_SUPPORT_CLASS_NAME =
13+
"com.oracle.svm.core.jdk.NativeLibrarySupport";
14+
private static final String LIB_LOADER_CLASS_NAME =
15+
"org.enso.interpreter.runtime.nativeimage.NativeLibrarySearchPath";
16+
private static final String USR_PATH_GETTER_FIELD_NAME = "usrPathsGetter";
17+
private static final String USR_PATH_SETTER_FIELD_NAME = "usrPathsSetter";
18+
private static final String NATIVE_LIBS_INSTANCE_FIELD_NAME = "nativeLibsInstance";
19+
private static final String USR_PATHS_FIELD_NAME = "usrPaths";
20+
21+
@Override
22+
public void beforeAnalysis(BeforeAnalysisAccess access) {
23+
var nativeLibsClass = access.findClassByName(NATIVE_LIBS_CLASS_NAME);
24+
var nativeLibSupportClass = access.findClassByName(NATIVE_LIBS_SUPPORT_CLASS_NAME);
25+
assert nativeLibSupportClass != null;
26+
var lookup = MethodHandles.lookup();
27+
MethodHandle getterMethodHandle;
28+
MethodHandle setterMethodHandle;
29+
try {
30+
var privateLookup = MethodHandles.privateLookupIn(nativeLibsClass, lookup);
31+
getterMethodHandle =
32+
privateLookup.findGetter(nativeLibSupportClass, USR_PATHS_FIELD_NAME, String[].class);
33+
setterMethodHandle =
34+
privateLookup.findSetter(nativeLibSupportClass, USR_PATHS_FIELD_NAME, String[].class);
35+
} catch (IllegalAccessException | NoSuchFieldException e) {
36+
throw new AssertionError(e);
37+
}
38+
var libLoaderClass = access.findClassByName(LIB_LOADER_CLASS_NAME);
39+
Field usrPathsGetterField;
40+
Field usrPathsSetterField;
41+
Field nativeLibsInstanceField;
42+
try {
43+
usrPathsGetterField = libLoaderClass.getDeclaredField(USR_PATH_GETTER_FIELD_NAME);
44+
usrPathsSetterField = libLoaderClass.getDeclaredField(USR_PATH_SETTER_FIELD_NAME);
45+
nativeLibsInstanceField = libLoaderClass.getDeclaredField(NATIVE_LIBS_INSTANCE_FIELD_NAME);
46+
} catch (NoSuchFieldException e) {
47+
throw new AssertionError(e);
48+
}
49+
var singleton = ImageSingletons.lookup(nativeLibSupportClass);
50+
access.registerFieldValueTransformer(nativeLibsInstanceField, (receiver, origVal) -> singleton);
51+
access.registerFieldValueTransformer(
52+
usrPathsGetterField, (receiver, origVal) -> getterMethodHandle);
53+
access.registerFieldValueTransformer(
54+
usrPathsSetterField, (receiver, origVal) -> setterMethodHandle);
55+
}
56+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package org.enso.interpreter.runtime.nativeimage;
2+
3+
import java.lang.invoke.MethodHandle;
4+
import java.nio.file.Files;
5+
import java.nio.file.Path;
6+
import org.graalvm.nativeimage.ImageInfo;
7+
import org.graalvm.nativeimage.ImageSingletons;
8+
9+
/**
10+
* Utility class for handling <emph>native library search path</emph> changes at runtime. Changing
11+
* the search path works only in NI.
12+
*
13+
* <p>Semantically the same as changing the {@code java.library.path} system property, and ensuring
14+
* the property change is reflected by the JVM.
15+
*
16+
* <p>Inspired by <a
17+
* href="https://github.com/Akirathan/native-lib-loader">Akirathan/native-lib-loader</a>
18+
*/
19+
public final class NativeLibrarySearchPath {
20+
private NativeLibrarySearchPath() {}
21+
22+
/** Getter for field {@code com.oracle.svm.core.jdk.NativeLibraries#usrPaths}. */
23+
static MethodHandle usrPathsGetter;
24+
25+
/** Setter for field {@code com.oracle.svm.core.jdk.NativeLibraries#usrPaths}. */
26+
static MethodHandle usrPathsSetter;
27+
28+
/**
29+
* {@link ImageSingletons singleton} instance of {@code
30+
* com.oracle.svm.core.jdk.NativeLibrarySupport}. This instance has {@code usrPaths} field that we
31+
* want to change at runtime. Note that this field needs to be {@code final}, otherwise, NI build
32+
* fails.
33+
*/
34+
static final Object nativeLibsInstance = null;
35+
36+
/**
37+
* Adds the given {@code path} directory to the search path for native libraries. Semantically, it
38+
* is equivalent to changing the {@code java.library.path} system property. Only works in Native
39+
* Image.
40+
*
41+
* @param path Directory to add to the search path.
42+
*/
43+
public static void addToSearchPath(String path) {
44+
if (ImageInfo.inImageRuntimeCode()) {
45+
assert Files.isDirectory(Path.of(path));
46+
addPath(path);
47+
}
48+
}
49+
50+
private static String[] getFieldValue() {
51+
try {
52+
return (String[]) usrPathsGetter.invoke(nativeLibsInstance);
53+
} catch (Throwable e) {
54+
throw new IllegalStateException(e);
55+
}
56+
}
57+
58+
private static void addPath(String path) {
59+
var oldValue = getFieldValue();
60+
var newValue = new String[oldValue.length + 1];
61+
System.arraycopy(oldValue, 0, newValue, 0, oldValue.length);
62+
newValue[newValue.length - 1] = path;
63+
setFieldValue(newValue);
64+
}
65+
66+
private static void setFieldValue(String[] newValue) {
67+
try {
68+
usrPathsSetter.invoke(nativeLibsInstance, newValue);
69+
} catch (Throwable e) {
70+
throw new IllegalStateException(e);
71+
}
72+
}
73+
}

engine/runtime/src/main/scala/org/enso/interpreter/runtime/DefaultPackageRepository.scala

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package org.enso.interpreter.runtime
22

33
import scala.jdk.OptionConverters.RichOption
4-
54
import org.enso.common.HostEnsoUtils
65
import org.enso.compiler.PackageRepository
76
import org.enso.compiler.context.CompilerContext
@@ -19,7 +18,10 @@ import org.enso.logger.masking.MaskedPath
1918
import org.enso.pkg.{
2019
Component,
2120
ComponentGroup,
21+
ComponentGroups,
2222
ExtendedComponentGroup,
23+
NativeLibraryFinder,
24+
Package,
2325
PackageManager,
2426
QualifiedName,
2527
SourceFile
@@ -29,16 +31,21 @@ import org.enso.common.CompilationStage
2931

3032
import java.nio.file.Path
3133
import scala.collection.immutable.ListSet
32-
import scala.jdk.CollectionConverters.{IterableHasAsJava, SeqHasAsJava}
34+
import scala.jdk.CollectionConverters.{
35+
CollectionHasAsScala,
36+
IterableHasAsJava,
37+
SeqHasAsJava
38+
}
3339
import scala.util.{Failure, Success, Try, Using}
3440
import org.enso.distribution.locking.ResourceManager
3541
import org.enso.distribution.{DistributionManager, LanguageHome}
3642
import org.enso.editions.updater.EditionManager
3743
import org.enso.editions.{DefaultEdition, Editions, LibraryName}
3844
import org.enso.interpreter.runtime.builtin.Builtins
3945
import org.enso.interpreter.runtime.instrument.NotificationHandler
46+
import org.enso.interpreter.runtime.nativeimage.NativeLibrarySearchPath
4047
import org.enso.librarymanager.DefaultLibraryProvider
41-
import org.enso.pkg.{ComponentGroups, Package}
48+
import org.graalvm.nativeimage.ImageInfo
4249
import org.slf4j.LoggerFactory
4350

4451
/** The default [[PackageRepository]] implementation.
@@ -232,11 +239,34 @@ private class DefaultPackageRepository(
232239
if (isLibrary) {
233240
val root = Path.of(pkg.root.toString)
234241
notificationHandler.addedLibrary(libraryName, libraryVersion, root)
242+
addNativeLibPath(pkg)
235243
}
236244

237245
loadedPackages.put(libraryName, Some(pkg))
238246
}
239247

248+
/** If the package contains any native libraries, their parent directories are added to the
249+
* native library search path. This only works in native image.
250+
* @param pkg the package to check for native libraries
251+
*/
252+
private def addNativeLibPath(
253+
pkg: Package[TruffleFile]
254+
): Unit = {
255+
if (ImageInfo.inImageRuntimeCode()) {
256+
val nativeLibs = NativeLibraryFinder.listAllNativeLibraries(
257+
pkg,
258+
TruffleFileSystem.INSTANCE
259+
)
260+
val distinctParentDirs = nativeLibs.asScala
261+
.map(_.getParent)
262+
.toSet
263+
distinctParentDirs.foreach { dir =>
264+
logger.debug("Adding '{}' to native lib search path", dir.getPath)
265+
NativeLibrarySearchPath.addToSearchPath(dir.getPath)
266+
}
267+
}
268+
}
269+
240270
/** For any given source file, infer data necessary to generate synthetic modules as well as their contents.
241271
* E.g., for A/B/C.enso it infers modules
242272
* - A.B that exports A.B.C

0 commit comments

Comments
 (0)