From multiarch-scala
Load native shared libraries on JVM via multiarch-core NativeLibLoader — do NOT reimplement OS detection, extraction, or System.load
How this skill is triggered — by the user, by Claude, or both
Slash command
/multiarch-scala:jvm-native-loadingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
When a JVM project needs to load native shared libraries (`.so`, `.dylib`,
When a JVM project needs to load native shared libraries (.so, .dylib,
.dll) at runtime — whether via Panama FFM or JNI — use the existing
multiarch.core.NativeLibLoader. Do NOT write custom code for:
System.getProperty("os.name"), os.arch)lib prefix, .so/.dylib/.dll extension)System.load() / System.loadLibrary() callsfindLibrary reflectionAll of this is already implemented, tested across 9 platforms, thread-safe,
and standardized in the multiarch-core library.
Two things on the classpath — the loader library and a provider JAR:
libraryDependencies ++= Seq(
// The loader (provides NativeLibLoader)
"com.kubuszok" %% "multiarch-core" % "0.1.2",
// A provider JAR containing the shared libraries + manifest
"com.kubuszok" % "pnm-provider-mylib-desktop" % "1.0.0"
)
The provider JAR is a plain JAR with this layout:
pnm-provider-mylib-desktop.jar
├── pnm-provider.json (manifest)
└── native/
├── linux-x86_64/libmylib.so
├── linux-aarch64/libmylib.so
├── macos-x86_64/libmylib.dylib
├── macos-aarch64/libmylib.dylib
├── windows-x86_64/mylib.dll
└── windows-aarch64/mylib.dll
import java.lang.foreign.*
object MyLibPlatform {
private val linker: Linker = Linker.nativeLinker()
private val libLookup: SymbolLookup = {
val libPath = multiarch.core.NativeLibLoader.load("mylib")
SymbolLookup.libraryLookup(libPath, Arena.global())
}
private def sym(name: String): MemorySegment =
libLookup.find(name).orElseThrow(() =>
new UnsupportedOperationException(s"Symbol not found: $name"))
// Bind C functions as MethodHandles
private val myFunction: java.lang.invoke.MethodHandle =
linker.downcallHandle(
sym("my_function"),
FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS)
)
}
That's it. NativeLibLoader.load("mylib") handles everything:
libmylib.so, libmylib.dylib, mylib.dll)java.library.path first (for dev overrides)native/<platform>/<mapped-name>Path to the extracted libraryYou then pass that Path to SymbolLookup.libraryLookup() for Panama
or to System.load(path.toString) for JNI.
| Provider type | Manifest file | Use case | Loading |
|---|---|---|---|
| Panama | pnm-provider.json | Panama FFM (java.lang.foreign) | NativeLibLoader at runtime |
| JNI | jni-provider.json | JNI (System.load) | NativeLibLoader at runtime |
| Scala Native | sn-provider.json | Static linking | sbt plugin at compile time (NOT NativeLibLoader) |
Panama and JNI providers work identically — the different manifest names only help prevent mixing up artifacts when a library has both static and dynamic variants.
NativeLibLoader.load(libName: String): PathResolves and extracts a single library. Returns the filesystem path.
Use this when you need the path for SymbolLookup.libraryLookup().
val path = multiarch.core.NativeLibLoader.load("mylib")
// path = /tmp/multiarch-native-xxxxx/libmylib.so (on Linux)
NativeLibLoader.loadAll(providerType: ProviderType): UnitAuto-discovers all provider manifests of the given type on the classpath,
extracts and loads every library declared for the current platform.
Calls System.load() on each one.
// Load everything from all pnm-provider.json manifests on classpath
multiarch.core.NativeLibLoader.loadAll(multiarch.core.ProviderType.Panama)
NativeLibLoader.loadConfigs(providerType: ProviderType, configNames: Set[String]): UnitLike loadAll, but only loads libraries from configs with matching names.
Use when a provider JAR bundles multiple libraries and you only need some.
multiarch.core.NativeLibLoader.loadConfigs(
multiarch.core.ProviderType.Panama,
Set("tree_sitter_core", "tree_sitter_java")
)
NativeLibLoader.load("mylib") searches in this order:
java.library.path — scans each directory for the mapped file name.
Use this for dev overrides (-Djava.library.path=/my/local/libs).
Classpath resource — looks for native/<host-classifier>/<mapped-name>
in all JARs and resource directories. This is where provider JARs deliver
their libraries.
Android system loader — if running on Android, uses the class loader's
findLibrary method and System.loadLibrary as fallback.
Error — throws UnsatisfiedLinkError with a diagnostic message listing
all searched locations.
Automatic, based on System.getProperty("os.name") and os.arch:
| Host | Classifier |
|---|---|
| Linux x86_64 | linux-x86_64 |
| Linux aarch64 | linux-aarch64 |
| macOS x86_64 | macos-x86_64 |
| macOS aarch64 | macos-aarch64 |
| Windows x86_64 | windows-x86_64 |
| Windows aarch64 | windows-aarch64 |
| Android aarch64 | android-aarch64 |
| Android armv7 | android-armv7 |
| Android x86_64 | android-x86_64 |
| OS | Input | Mapped file name |
|---|---|---|
| Linux | "mylib" | libmylib.so |
| macOS | "mylib" | libmylib.dylib |
| Windows | "mylib" | mylib.dll |
This is handled by System.mapLibraryName() (Unix) and a Windows-specific
override. You pass the logical name ("mylib"), not the file name.
From SGE — loading a Rust-built native ops library:
// sge/src/main/scalajvm/sge/platform/BufferOpsPanama.scala
private[platform] class BufferOpsPanama(val p: PanamaProvider) extends BufferOps {
private val linker: p.Linker = p.Linker.nativeLinker()
private val lib: p.SymbolLookup = {
val found = multiarch.core.NativeLibLoader.load("sge_native_ops")
p.SymbolLookup.libraryLookup(found, p.Arena.global())
}
private def lookup(name: String): p.MemorySegment =
lib.findOrThrow(name)
private val hAllocMemory: MethodHandle = linker.downcallHandle(
lookup("sge_alloc_memory"),
p.FunctionDescriptor.of(p.ADDRESS, p.JAVA_INT)
)
}
From SSG — loading tree-sitter via Panama:
// ssg-highlight/src/main/scalajvm/ssg/highlight/TreeSitterPlatformImpl.scala
object TreeSitterPlatformImpl extends TreeSitterPlatform {
private val nativeLinker: Linker = Linker.nativeLinker()
private val libLookup: SymbolLookup = {
val libPath = multiarch.core.NativeLibLoader.load("tree_sitter_all")
SymbolLookup.libraryLookup(libPath, Arena.global())
}
private def sym(name: String): MemorySegment =
libLookup.find(name).orElseThrow(...)
}
SGE loads ANGLE (GLESv2), GLFW, and miniaudio from separate provider JARs:
// build.sbt — provider JARs as dependencies
libraryDependencies ++= Seq(
"com.kubuszok" %% "multiarch-core" % "0.1.2",
"com.kubuszok" % "pnm-provider-sge-desktop" % "0.1.2"
)
// Scala code — load each library individually
val glesPath = multiarch.core.NativeLibLoader.load("GLESv2")
val glfwPath = multiarch.core.NativeLibLoader.load("glfw")
val audioPath = multiarch.core.NativeLibLoader.load("miniaudio")
val path = multiarch.core.NativeLibLoader.load("mylib")
System.load(path.toAbsolutePath.toString)
// Now JNI native methods are available
val jvmSettings = Seq(
fork := true, // REQUIRED for native lib loading
javaOptions ++= Seq(
"--enable-native-access=ALL-UNNAMED" // Required for Panama FFM
)
)
Without fork := true, sbt runs your code in its own JVM process and
java.library.path points to sbt's directories, not your project's.
For local development where libraries are built outside the provider JAR:
javaOptions += s"-Djava.library.path=${baseDirectory.value}/native-libs/target/release"
NativeLibLoader checks java.library.path first, so locally-built
libraries take precedence over classpath resources.
{
"provider-schema-version": "0.1.0",
"provider-name": "mylib",
"configs": [
{
"config-name": "mylib",
"linux-x86_64": { "binary": "libmylib.so" },
"linux-aarch64": { "binary": "libmylib.so" },
"macos-x86_64": { "binary": "libmylib.dylib" },
"macos-aarch64": { "binary": "libmylib.dylib" },
"windows-x86_64": { "binary": "mylib.dll" },
"windows-aarch64": { "binary": "mylib.dll" }
}
]
}
lazy val myProvider = project.in(file("my-provider"))
.settings(
name := "pnm-provider-mylib-desktop",
autoScalaLibrary := false,
crossPaths := false,
Compile / resourceDirectory := baseDirectory.value / "src" / "main" / "resources",
Compile / packageBin / mappings ++= {
val nativesDir = baseDirectory.value / "natives"
multiarch.core.Platform.desktop.flatMap { p =>
val platDir = nativesDir / p.classifier
if (platDir.exists())
IO.listFiles(platDir).filter(_.isFile)
.map(f => f -> s"native/${p.classifier}/${f.getName}").toSeq
else Seq.empty
}
}
)
// WRONG — this already exists in NativeLibLoader
val os = System.getProperty("os.name").toLowerCase match {
case n if n.contains("mac") => "macos"
case n if n.contains("linux") => "linux"
case n if n.contains("win") => "windows"
}
val arch = System.getProperty("os.arch") match {
case "amd64" | "x86_64" => "x86_64"
case "aarch64" | "arm64" => "aarch64"
}
val libName = os match {
case "macos" => s"lib$name.dylib"
case "linux" => s"lib$name.so"
case "windows" => s"$name.dll"
}
// WRONG — NativeLibLoader already does this
val stream = getClass.getResourceAsStream(s"/native/$os-$arch/$libName")
val tmpFile = File.createTempFile("native-", libName)
Files.copy(stream, tmpFile.toPath, StandardCopyOption.REPLACE_EXISTING)
System.load(tmpFile.getAbsolutePath)
// WRONG — System.loadLibrary searches java.library.path only
System.loadLibrary("mylib")
// RIGHT — NativeLibLoader searches classpath resources too
val path = multiarch.core.NativeLibLoader.load("mylib")
System.load(path.toAbsolutePath.toString)
// RIGHT — one line, handles everything
val path = multiarch.core.NativeLibLoader.load("mylib")
val lookup = SymbolLookup.libraryLookup(path, Arena.global())
NativeLibLoader throws a diagnostic error listing what it searched:
UnsatisfiedLinkError: Cannot find native library 'libmylib.so' (logical name: 'mylib').
Searched java.library.path: /usr/lib:/usr/local/lib
Searched classpath resource: native/macos-aarch64/libmylib.dylib
Host platform: macos-aarch64
This tells you exactly what's missing — usually the provider JAR isn't on the classpath or doesn't include your platform.
The library loaded successfully but SymbolLookup.find("my_function")
returns empty. This means the library doesn't export that symbol.
Check with nm -D libmylib.so | grep my_function (Linux/macOS).
Ensure fork := true is set for both Compile and Test:
fork := true
Test / fork := true
Test / javaOptions += "--enable-native-access=ALL-UNNAMED"
npx claudepluginhub kubuszok/multiarch-scala --plugin multiarch-scalaCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.