Unverified Commit ebacd4a4 authored by Nikolay Igotti's avatar Nikolay Igotti Committed by GitHub

Allow loading Skiko in more than one classloader. (#121)

Allow loading Skiko in more than one classloader.

Fixes https://github.com/JetBrains/compose-jb/issues/785.
parent 9ac8f685
...@@ -582,9 +582,12 @@ tasks.withType<AbstractPublishToMaven>().configureEach { ...@@ -582,9 +582,12 @@ tasks.withType<AbstractPublishToMaven>().configureEach {
tasks.withType<Test>().configureEach { tasks.withType<Test>().configureEach {
dependsOn(project.tasks.withType(LinkSharedLibrary::class.java).single { it.name.contains(buildType.id) }) dependsOn(project.tasks.withType(LinkSharedLibrary::class.java).single { it.name.contains(buildType.id) })
dependsOn(skikoJvmRuntimeJar)
options { options {
val dir = skikoNativeLib.parentFile.absolutePath val dir = skikoNativeLib.parentFile.absolutePath
systemProperty("skiko.library.path", dir) systemProperty("skiko.library.path", dir)
val jar = skikoJvmRuntimeJar.get().outputs.files.files.single { it.name.endsWith(".jar")}
systemProperty("skiko.jar.path", jar.absolutePath)
} }
} }
......
...@@ -9,19 +9,39 @@ import java.nio.file.StandardCopyOption ...@@ -9,19 +9,39 @@ import java.nio.file.StandardCopyOption
object Library { object Library {
private val skikoLibraryPath = System.getProperty("skiko.library.path") private val skikoLibraryPath = System.getProperty("skiko.library.path")
private val cacheRoot = "${System.getProperty("user.home")}/.skiko/" private val cacheRoot = "${System.getProperty("user.home")}/.skiko/"
private var copyDir: File? = null
private fun loadOrGet(cacheDir: File, path: String, resourceName: String, isLibrary: Boolean) { // Same native library cannot be loaded in several classloaders, so we have to clone
val file = File(cacheDir, resourceName) // native library to allow Skiko loading to work properly in complex cases, i.e.
// several IDEA plugins.
private fun loadLibraryOrCopy(library: File) {
try {
System.load(library.absolutePath)
} catch (e: UnsatisfiedLinkError) {
if (e.message?.contains("already loaded in another classloader") == true) {
val tempFile = File.createTempFile("skiko", if (hostOs.isWindows) ".dll" else "")
copyDir = tempFile.parentFile
Files.copy(library.toPath(), tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
tempFile.deleteOnExit()
System.load(tempFile.absolutePath)
} else {
throw e
}
}
}
private fun unpackIfNeeded(dest: File, resourceName: String, deleteOnExit: Boolean): File {
val file = File(dest, resourceName)
if (!file.exists()) { if (!file.exists()) {
val tempFile = File.createTempFile("skiko", "", cacheDir) val tempFile = File.createTempFile("skiko", "", dest)
Library::class.java.getResourceAsStream("$path$resourceName").use { input -> if (deleteOnExit)
file.deleteOnExit()
Library::class.java.getResourceAsStream("/$resourceName").use { input ->
Files.copy(input, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING) Files.copy(input, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
} }
Files.move(tempFile.toPath(), file.toPath(), StandardCopyOption.ATOMIC_MOVE) Files.move(tempFile.toPath(), file.toPath(), StandardCopyOption.ATOMIC_MOVE)
} }
if (isLibrary) { return file
System.load(file.absolutePath)
}
} }
// This function does the following: on request to load given resource, // This function does the following: on request to load given resource,
...@@ -32,14 +52,17 @@ object Library { ...@@ -32,14 +52,17 @@ object Library {
fun load() { fun load() {
val name = "skiko-$hostId" val name = "skiko-$hostId"
val platformName = System.mapLibraryName(name) val platformName = System.mapLibraryName(name)
val icu = if (hostOs.isWindows) "icudtl.dat" else null
if (skikoLibraryPath != null) { if (skikoLibraryPath != null) {
val library = File(File(skikoLibraryPath), platformName) val library = File(File(skikoLibraryPath), platformName)
System.load(library.absolutePath) loadLibraryOrCopy(library)
if (icu != null && copyDir != null) {
unpackIfNeeded(copyDir!!, icu, true)
}
} else { } else {
val resourcePath = "/"
val hashResourceStream = Library::class.java.getResourceAsStream( val hashResourceStream = Library::class.java.getResourceAsStream(
"$resourcePath$platformName.sha256" "/$platformName.sha256"
) ?: throw LibraryLoadException( ) ?: throw LibraryLoadException(
"Cannot find $platformName.sha256, proper native dependency missing." "Cannot find $platformName.sha256, proper native dependency missing."
) )
...@@ -48,10 +71,10 @@ object Library { ...@@ -48,10 +71,10 @@ object Library {
} }
val cacheDir = File(File(cacheRoot), hash) val cacheDir = File(File(cacheRoot), hash)
cacheDir.mkdirs() cacheDir.mkdirs()
loadOrGet(cacheDir, resourcePath, platformName, true) val library = unpackIfNeeded(cacheDir, platformName, false)
val loadIcu = hostOs.isWindows loadLibraryOrCopy(library)
if (loadIcu) { if (icu != null) {
loadOrGet(cacheDir, resourcePath, "icudtl.dat", false) unpackIfNeeded(cacheDir, icu, false)
} }
} }
...@@ -70,3 +93,11 @@ object Library { ...@@ -70,3 +93,11 @@ object Library {
} }
} }
} }
// We have to keep this tiny class in Skiko for testing purposes.
internal class LibraryTestImpl() {
fun run(): Long {
val bitmap = org.jetbrains.skija.Bitmap()
return bitmap._ptr
}
}
\ No newline at end of file
package org.jetbrains.skiko
import org.junit.Test
import java.net.URL
import java.net.URLClassLoader
import java.nio.file.Paths
import kotlin.concurrent.thread
private class PlatformAndURLClassLoader(classpath: List<URL>) :
ClassLoader(getPlatformClassLoader()) {
private val childClassLoader: ChildURLClassLoader
private class FindClassClassLoader(parent: ClassLoader?) : ClassLoader(parent) {
@Throws(ClassNotFoundException::class)
public override fun findClass(name: String): Class<*> {
return super.findClass(name)
}
}
private class ChildURLClassLoader(urls: Array<URL>?, private val realParent: FindClassClassLoader) :
URLClassLoader(urls, null) {
@Throws(ClassNotFoundException::class)
public override fun findClass(name: String): Class<*> {
return try {
super.findClass(name)
} catch (e: ClassNotFoundException) {
realParent.loadClass(name)
}
}
}
@Synchronized
@Throws(ClassNotFoundException::class)
override fun loadClass(name: String, resolve: Boolean): Class<*> {
return try {
childClassLoader.findClass(name)
} catch (e: ClassNotFoundException) {
super.loadClass(name, resolve)
}
}
init {
val urls = classpath.toTypedArray()
childClassLoader = ChildURLClassLoader(
urls, FindClassClassLoader(
parent
)
)
}
}
private fun testSkikoLoad(loader: ClassLoader) {
val clazz = loader.loadClass("org.jetbrains.skiko.LibraryTestImpl")
val tester = clazz.getDeclaredConstructor().newInstance()
val ptr = clazz.getMethod("run").invoke(tester) as Long
assert(ptr != 0L)
}
class SeveralClassloaders {
@Test
fun `load skiko in several classloaders`() {
val threaded = false
val jar = System.getProperty("skiko.jar.path")
val stdlibClass = Class.forName("kotlin.jvm.internal.Intrinsics")
val stdLibJar = stdlibClass.protectionDomain.codeSource.location
val urls = listOf(Paths.get(jar).toUri().toURL(), stdLibJar)
val loaders = arrayOf(PlatformAndURLClassLoader(urls), PlatformAndURLClassLoader(urls))
if (threaded) {
val threads = mutableListOf<Thread>()
loaders.forEach { classloader ->
threads.add(thread {
Thread.currentThread().contextClassLoader = classloader
testSkikoLoad(classloader)
})
}
threads.forEach {
it.join()
}
} else {
loaders.forEach { classloader ->
testSkikoLoad(classloader)
}
}
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment