Skip to content

Commit

Permalink
ffi: migrate to java.lang.foreign and require Java 21
Browse files Browse the repository at this point in the history
  • Loading branch information
azenla committed Sep 23, 2023
1 parent c340cfb commit f8ba7af
Show file tree
Hide file tree
Showing 26 changed files with 270 additions and 798 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- name: Set up JDK 17
- name: Set up JDK 20
uses: actions/setup-java@v3
with:
java-version: '17'
java-version: '20'
distribution: 'temurin'
- name: Build with Gradle
uses: gradle/gradle-build-action@v2
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/graal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
- name: Set up GraalVM
uses: graalvm/setup-graalvm@v1
with:
java-version: '17'
java-version: '20'
distribution: 'graalvm'
- name: Build with Gradle
uses: gradle/gradle-build-action@v2
Expand All @@ -33,7 +33,7 @@ jobs:
- name: Set up GraalVM
uses: graalvm/setup-graalvm@v1
with:
java-version: '17'
java-version: '20'
distribution: 'graalvm'
- name: Build with Gradle
uses: gradle/gradle-build-action@v2
Expand All @@ -57,7 +57,7 @@ jobs:
- name: Set up GraalVM
uses: graalvm/setup-graalvm@v1
with:
java-version: '17'
java-version: '20'
distribution: 'graalvm'
- name: Build with Gradle
uses: gradle/gradle-build-action@v2
Expand Down
5 changes: 2 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
plugins {
kotlin("jvm") version "1.9.10" apply false
kotlin("plugin.serialization") version "1.9.10" apply false
id("gay.pizza.pork.root")
}

tasks.withType<Wrapper> {
gradleVersion = "8.3"
gradleVersion = "8.4-rc-1"
}
24 changes: 21 additions & 3 deletions buildext/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@file:Suppress("UnstableApiUsage")
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
`kotlin-dsl`
embeddedKotlin("plugin.serialization")
Expand All @@ -10,14 +11,31 @@ repositories {
}

dependencies {
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10")
implementation("org.jetbrains.kotlin:kotlin-serialization:1.9.10")
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20-Beta2")
implementation("org.jetbrains.kotlin:kotlin-serialization:1.9.20-Beta2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation("com.charleskorn.kaml:kaml:0.55.0")
}

java {
sourceCompatibility = JavaVersion.VERSION_20
targetCompatibility = JavaVersion.VERSION_20
}

tasks.withType<KotlinCompile> {
kotlinOptions.jvmTarget = "20"
}

gradlePlugin {
plugins {
create("pork_root") {
id = "gay.pizza.pork.root"
implementationClass = "gay.pizza.pork.buildext.PorkRootPlugin"

displayName = "Pork Root"
description = "Root convention for pork"
}

create("pork_ast") {
id = "gay.pizza.pork.ast"
implementationClass = "gay.pizza.pork.buildext.PorkAstPlugin"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.kotlin.dsl.*
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

open class PorkModulePlugin : Plugin<Project> {
Expand All @@ -16,13 +17,17 @@ open class PorkModulePlugin : Plugin<Project> {
target.repositories.maven(url = "https://gitlab.com/api/v4/projects/49101454/packages/maven")

target.extensions.getByType<JavaPluginExtension>().apply {
val javaVersion = JavaVersion.toVersion(17)
val javaVersion = JavaVersion.toVersion(21)
sourceCompatibility = javaVersion
targetCompatibility = javaVersion
}

target.tasks.withType<KotlinCompile> {
kotlinOptions.jvmTarget = "17"
kotlinOptions.jvmTarget = "21"
}

target.extensions.getByType<KotlinJvmProjectExtension>().apply {
jvmToolchain(21)
}

target.dependencies {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package gay.pizza.pork.buildext

import org.gradle.api.Plugin
import org.gradle.api.Project

class PorkRootPlugin : Plugin<Project> {
override fun apply(target: Project) {}
}
1 change: 0 additions & 1 deletion ffi/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,4 @@ dependencies {
api(project(":evaluator"))

implementation(project(":common"))
implementation("net.java.dev.jna:jna:5.13.0")
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ class FfiFunctionDefinition(
library,
functionName,
returnType,
parameterString.split(",").map { it.trim() }
parameterString.splitToSequence(",")
.map { it.trim() }
.filter { it.isNotEmpty() }
.toList()
)
}
}
Expand Down
46 changes: 46 additions & 0 deletions ffi/src/main/kotlin/gay/pizza/pork/ffi/FfiLibraryCache.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package gay.pizza.pork.ffi

import java.lang.foreign.*

object FfiLibraryCache {
private val dlopenFunctionDescriptor = FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT)
private val dlsymFunctionDescriptor = FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS)

private val dlopenMemorySegment = Linker.nativeLinker().defaultLookup().find("dlopen").orElseThrow()
private val dlsymMemorySegment = Linker.nativeLinker().defaultLookup().find("dlsym").orElseThrow()

private val dlopen = Linker.nativeLinker().downcallHandle(
dlopenMemorySegment,
dlopenFunctionDescriptor
)

private val dlsym = Linker.nativeLinker().downcallHandle(
dlsymMemorySegment,
dlsymFunctionDescriptor
)

private val libraryHandles = mutableMapOf<String, MemorySegment>()

private fun dlopen(name: String): MemorySegment {
var handle = libraryHandles[name]
if (handle != null) {
return handle
}
return Arena.ofConfined().use { arena ->
val nameStringPointer = arena.allocateUtf8String(name)
handle = dlopen.invokeExact(nameStringPointer, 0) as MemorySegment
if (handle == MemorySegment.NULL) {
throw RuntimeException("Unable to dlopen library: $name")
}
handle!!
}
}

fun dlsym(name: String, symbol: String): MemorySegment {
val libraryHandle = dlopen(name)
return Arena.ofConfined().use { arena ->
val symbolStringPointer = arena.allocateUtf8String(symbol)
dlsym.invokeExact(libraryHandle, symbolStringPointer) as MemorySegment
}
}
}
25 changes: 25 additions & 0 deletions ffi/src/main/kotlin/gay/pizza/pork/ffi/FfiMacPlatform.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package gay.pizza.pork.ffi

import java.nio.file.Path
import kotlin.io.path.*

object FfiMacPlatform : FfiPlatform {
private val frameworksDirectories = listOf(
"/Library/Frameworks"
)

override fun findLibrary(name: String): Path? {
val frameworksToCheck = frameworksDirectories.map { frameworkDirectory ->
Path("$frameworkDirectory/$name.framework/$name")
}
for (framework in frameworksToCheck) {
if (!framework.exists()) continue
return if (framework.isSymbolicLink()) {
return framework.parent.resolve(framework.readSymbolicLink()).absolute()
} else {
framework.absolute()
}
}
return null
}
}
79 changes: 79 additions & 0 deletions ffi/src/main/kotlin/gay/pizza/pork/ffi/FfiNativeProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package gay.pizza.pork.ffi

import gay.pizza.pork.ast.ArgumentSpec
import gay.pizza.pork.evaluator.CallableFunction
import gay.pizza.pork.evaluator.NativeProvider
import gay.pizza.pork.evaluator.None
import java.lang.foreign.*
import java.nio.file.Path
import kotlin.io.path.Path
import kotlin.io.path.absolutePathString
import kotlin.io.path.exists

class FfiNativeProvider : NativeProvider {
private val ffiTypeRegistry = FfiTypeRegistry()

override fun provideNativeFunction(definitions: List<String>, arguments: List<ArgumentSpec>): CallableFunction {
val functionDefinition = FfiFunctionDefinition.parse(definitions[0], definitions[1])
val linker = Linker.nativeLinker()
val functionAddress = lookupSymbol(functionDefinition)

val parameters = functionDefinition.parameters.map { id ->
ffiTypeRegistry.lookup(id) ?: throw RuntimeException("Unknown ffi type: $id")
}

val returnTypeId = functionDefinition.returnType
val returnType = ffiTypeRegistry.lookup(returnTypeId) ?:
throw RuntimeException("Unknown ffi return type: $returnTypeId")
val parameterArray = parameters.map { typeAsLayout(it) }.toTypedArray()
val descriptor = if (returnType == FfiPrimitiveType.Void)
FunctionDescriptor.ofVoid(*parameterArray)
else FunctionDescriptor.of(typeAsLayout(returnType), *parameterArray)
val handle = linker.downcallHandle(functionAddress, descriptor)
return CallableFunction { functionArguments, _ ->
Arena.ofConfined().use { arena ->
handle.invokeWithArguments(functionArguments.map { valueAsFfi(it, arena) }) ?: None
}
}
}

private fun lookupSymbol(functionDefinition: FfiFunctionDefinition): MemorySegment {
if (functionDefinition.library == "c") {
return SymbolLookup.loaderLookup().find(functionDefinition.function).orElseThrow {
RuntimeException("Unknown function: ${functionDefinition.function}")
}
}
val actualLibraryPath = findLibraryPath(functionDefinition.library)
val functionAddress = FfiLibraryCache.dlsym(actualLibraryPath.absolutePathString(), functionDefinition.function)
if (functionAddress.address() == 0L) {
throw RuntimeException("Unknown function: ${functionDefinition.function} in library $actualLibraryPath")
}
return functionAddress
}

private fun typeAsLayout(type: FfiType): MemoryLayout = when (type) {
FfiPrimitiveType.UnsignedByte, FfiPrimitiveType.Byte -> ValueLayout.JAVA_BYTE
FfiPrimitiveType.UnsignedInt, FfiPrimitiveType.Int -> ValueLayout.JAVA_INT
FfiPrimitiveType.UnsignedShort, FfiPrimitiveType.Short -> ValueLayout.JAVA_SHORT
FfiPrimitiveType.UnsignedLong, FfiPrimitiveType.Long -> ValueLayout.JAVA_LONG
FfiPrimitiveType.String -> ValueLayout.ADDRESS
FfiPrimitiveType.Pointer -> ValueLayout.ADDRESS
FfiPrimitiveType.Void -> MemoryLayout.sequenceLayout(0, ValueLayout.JAVA_INT)
else -> throw RuntimeException("Unknown ffi type to convert to memory layout: $type")
}

private fun valueAsFfi(value: Any, allocator: SegmentAllocator): Any = when {
value is String -> allocator.allocateUtf8String(value)
value == None -> MemorySegment.NULL
else -> value
}

private fun findLibraryPath(name: String): Path {
val initialPath = Path(name)
if (initialPath.exists()) {
return initialPath
}
return FfiPlatforms.current.platform.findLibrary(name)
?: throw RuntimeException("Unable to find library: $name")
}
}
24 changes: 24 additions & 0 deletions ffi/src/main/kotlin/gay/pizza/pork/ffi/FfiPlatform.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package gay.pizza.pork.ffi

import java.nio.file.Path

enum class FfiPlatforms(val id: String, val platform: FfiPlatform) {
Mac("macOS", FfiMacPlatform),
Windows("Windows", FfiWindowsPlatform),
Unix("Unix", FfiUnixPlatform);

companion object {
val current by lazy {
val operatingSystemName = System.getProperty("os.name").lowercase()
when {
operatingSystemName.contains("win") -> Windows
operatingSystemName.contains("mac") -> Mac
else -> Unix
}
}
}
}

interface FfiPlatform {
fun findLibrary(name: String): Path?
}
12 changes: 7 additions & 5 deletions ffi/src/main/kotlin/gay/pizza/pork/ffi/FfiPrimitiveType.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package gay.pizza.pork.ffi

import gay.pizza.pork.evaluator.None
import java.lang.foreign.MemorySegment

enum class FfiPrimitiveType(
val id: kotlin.String,
override val size: kotlin.Int,
override val size: kotlin.Long,
val numberConvert: (Number.() -> Number)? = null,
val nullableConversion: (Any?.() -> Any)? = null,
val notNullConversion: (Any.() -> Any)? = null
Expand All @@ -22,9 +23,10 @@ enum class FfiPrimitiveType(
String("char*", 8, nullableConversion = { toString() }),
Pointer("void*", 8, nullableConversion = {
if (this is kotlin.Long) {
com.sun.jna.Pointer(this)
MemorySegment.ofAddress(this)
} else if (this == None) {
com.sun.jna.Pointer.NULL
} else this as com.sun.jna.Pointer
})
MemorySegment.NULL
} else this as MemorySegment
}),
Void("void", 0)
}
2 changes: 1 addition & 1 deletion ffi/src/main/kotlin/gay/pizza/pork/ffi/FfiStruct.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ class FfiStruct : FfiType {
fields.add(FfiStructField(field, type))
}

override val size: Int
override val size: Long
get() = fields.sumOf { it.type.size }
}
2 changes: 1 addition & 1 deletion ffi/src/main/kotlin/gay/pizza/pork/ffi/FfiType.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package gay.pizza.pork.ffi

interface FfiType {
val size: Int
val size: Long
}
18 changes: 18 additions & 0 deletions ffi/src/main/kotlin/gay/pizza/pork/ffi/FfiTypeRegistry.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package gay.pizza.pork.ffi

class FfiTypeRegistry {
private val types = mutableMapOf<String, FfiType>()

init {
for (type in FfiPrimitiveType.entries) {
add(type.id, type)
}
add("size_t", FfiPrimitiveType.Long)
}

fun add(name: String, type: FfiType) {
types[name] = type
}

fun lookup(name: String): FfiType? = types[name]
}
7 changes: 7 additions & 0 deletions ffi/src/main/kotlin/gay/pizza/pork/ffi/FfiUnixPlatform.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package gay.pizza.pork.ffi

import java.nio.file.Path

object FfiUnixPlatform : FfiPlatform {
override fun findLibrary(name: String): Path? = null
}
Loading

0 comments on commit f8ba7af

Please sign in to comment.