Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ffi: migrate to java.lang.foreign and require Java 20 #12

Merged
merged 1 commit into from
Oct 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 21
uses: actions/setup-java@v3
with:
java-version: '17'
java-version: '21'
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: '21'
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: '21'
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: '21'
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)
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
Loading