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

Split JVM syscall implemetations into separate modules, add FFM #200

Merged
merged 16 commits into from
Aug 18, 2024
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
6 changes: 3 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ jobs:
- uses: actions/checkout@v4
- uses: graalvm/setup-graalvm@v1
with:
java-version: 21
java-version: 22
distribution: 'graalvm-community'
set-java-home: false
- uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: 21
java-version: 22
- uses: gradle/actions/setup-gradle@v3
- name: Run tests
run: >-
Expand Down Expand Up @@ -62,7 +62,7 @@ jobs:
- uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: 21
java-version: 22
- uses: gradle/actions/setup-gradle@v3
- name: Deploy to sonatype
# disable configuration cache due to https://github.com/gradle/gradle/issues/22779
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: 21
java-version: 22
- uses: actions/setup-python@v5
with:
python-version: '3.12'
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
# Changelog

## Unreleased
## Added
- New `TerminalDetection` object that can be used to detect terminal capabilities without creating a terminal instance.
- Added new optional methods to `TerminalInterface` to control raw mode: `getTerminalSize`, `readInputEvent`, `enterRawMode`, and `shouldAutoUpdateSize`.
- Added new terminal implementation that uses the [Foreign Function and Memory (FFM) API](https://openjdk.java.net/jeps/419) added in JDK 22.
- Split the library up into modules, so you can produce smaller JVM artifacts by only using the parts you need.

### Changed
- **Breaking Change** Moved `Terminal.info.width` and `height` to `Terminal.size.width` and `height`.
- **Breaking Change** `TerminalInterface.info` is now a method with parameters instead of a property.

### Removed
- Removed constructor overloads for `Terminal`. There is now one constructor with all default parameters.

### Fixed
- Fixed ConcurrentModificationException from progress bars when updated under very high concurrency [(#240)](https://github.com/ajalt/mordant/issues/240)
Expand Down
Empty file modified LICENSE.txt
100755 → 100644
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ kotlin {

sourceSets {
jvmMain.dependencies {
implementation(project(":mordant"))
implementation(project(":mordant-omnibus"))
}
}
}

application {
mainClass.set("com.github.ajalt.mordant.samples.MainKt")
applicationDefaultJvmArgs = listOf("-Dfile.encoding=utf-8")
applicationDefaultJvmArgs = listOf(
"-Dfile.encoding=utf-8",
"--enable-native-access=ALL-UNNAMED"
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,8 @@ kotlin {
commonMain.dependencies {
implementation(project(":mordant"))
}
jvmMain.dependencies {
implementation(project(":mordant-omnibus"))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import com.vanniktech.maven.publish.JavadocJar
import com.vanniktech.maven.publish.KotlinMultiplatform
import com.vanniktech.maven.publish.SonatypeHost
import org.jetbrains.dokka.gradle.DokkaTaskPartial
import java.io.ByteArrayOutputStream

plugins {
id("com.vanniktech.maven.publish.base")
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[versions]
kotlin = "2.0.0"
kotlin = "2.0.10"
coroutines = "1.8.1"

[libraries]
Expand Down
5 changes: 5 additions & 0 deletions mordant-jvm-ffm/api/mordant-jvm-ffm.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
public final class com/github/ajalt/mordant/terminal/terminalinterface/ffm/TerminalInterfaceProviderFfm : com/github/ajalt/mordant/terminal/TerminalInterfaceProvider {
public fun <init> ()V
public fun load ()Lcom/github/ajalt/mordant/terminal/TerminalInterface;
}

16 changes: 16 additions & 0 deletions mordant-jvm-ffm/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
plugins {
id("mordant-kotlin-conventions")
id("mordant-publishing-conventions")
}

kotlin {
jvm()
sourceSets {
commonMain.dependencies {
implementation(project(":mordant"))
}
jvmMain.dependencies {
compileOnly(libs.graalvm.svm)
}
}
}
2 changes: 2 additions & 0 deletions mordant-jvm-ffm/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
POM_ARTIFACT_ID=mordant-jvm-ffm
POM_NAME=Mordant Foreign Function & Memory Interface
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package com.github.ajalt.mordant.terminal.terminalinterface.ffm

import java.lang.foreign.*
import java.lang.foreign.MemoryLayout.PathElement
import java.lang.invoke.MethodHandle
import java.lang.invoke.MethodHandles
import java.lang.invoke.VarHandle
import kotlin.properties.PropertyDelegateProvider
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty

internal object Layouts {
// val BOOL: ValueLayout.OfBoolean = ValueLayout.JAVA_BOOLEAN
val BYTE: ValueLayout.OfByte = ValueLayout.JAVA_BYTE
val SHORT: ValueLayout.OfShort = ValueLayout.JAVA_SHORT
val INT: ValueLayout.OfInt = ValueLayout.JAVA_INT
val LONG: ValueLayout.OfLong = ValueLayout.JAVA_LONG
// val FLOAT: ValueLayout.OfFloat = ValueLayout.JAVA_FLOAT
// val DOUBLE: ValueLayout.OfDouble = ValueLayout.JAVA_DOUBLE
val POINTER: AddressLayout = ValueLayout.ADDRESS
}

internal class FieldLayout<T>(
val name: String,
val layout: MemoryLayout,
val access: (MemorySegment) -> T,
val set: (MemorySegment, T) -> Unit = { _, _ -> },
)

internal abstract class StructLayout {
private val fields = mutableListOf<FieldLayout<*>>()

fun registerField(layout: FieldLayout<*>) {
fields.add(layout)
}

val layout: MemoryLayout
get() = MemoryLayout.structLayout(
*fields.map { it.layout.withName(it.name) }.toTypedArray()
)
}


@Suppress("UnusedReceiverParameter")
internal inline fun <reified T> StructLayout.scalarField(
layout: MemoryLayout,
crossinline convert: (Any) -> T = { it as T },
): PropertyDelegateProvider<StructLayout, ReadOnlyProperty<Any?, FieldLayout<T>>> {
return fieldDelegate(layout,
{ name, parent, segment -> convert(parent.layout.varHandle(name).get(segment)) },
{ name, parent, segment, value -> parent.layout.varHandle(name).set(segment, value) }
)
}

@Suppress("UnusedReceiverParameter")
internal inline fun <T : StructAccessor> StructLayout.structField(
field: StructLayout,
crossinline construct: (MemorySegment) -> T,
): PropertyDelegateProvider<StructLayout, ReadOnlyProperty<Any?, FieldLayout<T>>> {
return fieldDelegate(field.layout, { name, parent, segment ->
construct(segment.offsetOf(name, parent.layout, field.layout))
})
}

@Suppress("UnusedReceiverParameter")
internal inline fun <reified T> StructLayout.customField(
layout: MemoryLayout,
crossinline construct: (MemorySegment, parent: StructLayout) -> T,
): PropertyDelegateProvider<StructLayout, ReadOnlyProperty<Any?, FieldLayout<T>>> {
return fieldDelegate(layout,
{ _, parent, segment -> construct(segment, parent) }
)
}

private inline fun <T> fieldDelegate(
layout: MemoryLayout,
crossinline access: (name: String, parent: StructLayout, MemorySegment) -> T,
crossinline set: (name: String, parent: StructLayout, MemorySegment, T) -> Unit = { _, _, _, _ -> },
): PropertyDelegateProvider<StructLayout, ReadOnlyProperty<Any?, FieldLayout<T>>> {
return PropertyDelegateProvider { parent, property ->
val fl = FieldLayout(
property.name, layout,
{ segment -> access(property.name, parent, segment) },
{ segment, value -> set(property.name, parent, segment, value) },
)
parent.registerField(fl)
ReadOnlyProperty { _, _ -> fl }
}
}

internal fun StructLayout.byteField() = scalarField<Byte>(Layouts.BYTE)
internal fun StructLayout.shortField() = scalarField<Short>(Layouts.SHORT)
internal fun StructLayout.intField() = scalarField<Int>(Layouts.INT)
internal fun StructLayout.longField() = scalarField<Long>(Layouts.LONG)
//internal fun StructLayout.floatField() = scalarField<Float>(Layouts.FLOAT)
//internal fun StructLayout.doubleField() = scalarField<Double>(Layouts.DOUBLE)
//internal fun StructLayout.boolField() = scalarField<Boolean>(Layouts.BOOL)
@Suppress("UnusedReceiverParameter")
internal fun StructLayout.arrayField(
size: Long,
elementLayout: MemoryLayout = Layouts.BYTE,
): PropertyDelegateProvider<StructLayout, ReadOnlyProperty<Any?, FieldLayout<MemorySegment>>> {
return fieldDelegate(
MemoryLayout.sequenceLayout(size, elementLayout), { name, parent, segment ->
segment.asSlice(parent.layout.byteOffset(name), size * elementLayout.byteSize())
}
)
}

internal fun StructLayout.paddingField(size: Long) =
scalarField<Unit>(MemoryLayout.paddingLayout(size)) { }

internal interface StructAccessor {
val segment: MemorySegment

operator fun <T> FieldLayout<T>.getValue(thisRef: StructAccessor, property: KProperty<*>): T {
return access(thisRef.segment)
}

operator fun <T> FieldLayout<T>.setValue(
thisRef: StructAccessor,
property: KProperty<*>,
value: T,
) {
set(thisRef.segment, value)
}
}


internal fun Arena.allocateInt(): MemorySegment = allocate(ValueLayout.JAVA_INT)
internal fun MemoryLayout.varHandle(vararg path: String): VarHandle {
val handle = varHandle(*path.map { PathElement.groupElement(it) }.toTypedArray())
return MethodHandles.insertCoordinates(handle, handle.coordinateTypes().lastIndex, 0)
}

internal fun MemoryLayout.byteOffset(name: String): Long {
return byteOffset(PathElement.groupElement(name))
}

internal fun MemorySegment.offsetOf(
name: String,
parent: MemoryLayout,
layout: MemoryLayout,
): MemorySegment {
return asSlice(parent.byteOffset(name), layout.byteSize())
}

internal abstract class MethodHandlesHolder(
private val linker: Linker = Linker.nativeLinker(),
private val lookup: SymbolLookup = SymbolLookup.loaderLookup().or(linker.defaultLookup()),
) {
protected fun handle(
resLayout: MemoryLayout,
vararg argLayouts: MemoryLayout,
) =
PropertyDelegateProvider<MethodHandlesHolder, ReadOnlyProperty<Any?, MethodHandle>> { _, property ->
val name = property.name
ReadOnlyProperty { _, _ ->
lookup.find(name)
.map {
linker.downcallHandle(
it,
FunctionDescriptor.of(resLayout, *argLayouts)
)
}
.orElseThrow()
}
}
}
Loading