Closed philipguin closed 1 year ago
Hey @philipguin,
We're all eager to go to native interop heaven, with type-safe handle & enum types and zero overhead compared to C. It's true that Kotlin gets very close to it, but it's a solution that I always turn down, for two main reasons:
The good news is that a Java solution is coming in the future, which is value classes with Project Valhalla. I know it's been almost ten years since we first heard about it, but I've been following its progress very closely and am hopeful that we'll see a preview soon. The other good news is that, afaict, Kotlin is already designed and prepared to interoperate with value classes when they become available. So, whatever works for Java, will work equally well for Kotlin.
After the 3.3.2 release there will not be any development for new features in LWJGL 3, other than maintaining the existing bindings. The focus will slowly shift to LWJGL 4, which will be designed to work with projects Panama and Valhalla.
I too have been awaiting the glory of projects Panama and Valhalla, though the last Valhalla update I'm aware of is the blog update from 2020 - I would love to read a more recent update!
I'm not sure how passing value types across the JNI border/equivalent would work, though I suppose with Panama we won't have to worry about that. Declaring Panama structs looked relatively involved last I checked, but I suppose that's a problem for LWJGL, not its users (and LWJGL can simply generate them.)
I'm sure the release of both projects will have major implications for LWJGL, so good luck to you and other contributors!
Description
So right now, if you want to do things in LWJGL (or most JNI code) in an alloc-free way, you basically work like this:
But now that we have
@JvmInline value class
in Kotlin, we can achieve something very similar to the intended syntax in C, with zero allocs and zero overhead:I'm already using this pattern to wrap Assimp data structures in LWJGL, as well as my own PhysX bindings, and it's worked fantastically thus far. I think generating a bunch of these classes for just about everything in LWJGL would be a fantastic addition for Kotlin users, though I can't speak to other considerations.
In terms of code bloat, it may be possible to define
external fun
inside the value class, thus avoiding duplicate code files. Otherwise, it might be prudent to include these bindings in separate, optional downloadable JARs.This is probably a major request, but I do think the benefit of cleaner, more concise syntax is worthwhile!
Assimp code from my project
```kotlin @JvmInline value class AssimpNode(val ptr: Long) { val isNull get() = ptr == 0L val isNotNull get() = ptr != 0L val name get() = AssimpString(ptr + AINode.MNAME) fun allocName(): String = name.toString() fun readTransformation(dest: Matrix4f) = AssimpUtils.convertTransform(ptr + AINode.MTRANSFORMATION, dest) val parent get() = AssimpNode(_U.getAddress(ptr + AINode.MPARENT)) val numChildren get() = _U.getInt(ptr + AINode.MNUMCHILDREN) val children get() = AssimpNodeArray(_U.getAddress(ptr + AINode.MCHILDREN)) val numMeshes get() = _U.getInt(ptr + AINode.MNUMMESHES) val meshIndices get() = AssimpIntArray(_U.getAddress(ptr + AINode.MMESHES)) val metadata get() = AssimpMetadata(_U.getAddress(ptr + AINode.MMETADATA)) override fun toString(): String { if (ptr == 0L) return "null" val parent = if (parent.isNull) "null" else parent.name.toString() val children = Array(numChildren) { i -> children[i].takeIf { it.isNotNull }?.name?.toString() }.joinToString(", ", "[", "]") val meshIndices = IntArray(numMeshes) { meshIndices[it] }.contentToString() val transform = readTransformation(Matrix4f()).toString() return "AssimpNode(name: $name, parent: $parent, children: $children, meshIndices: $meshIndices, transform: $transform)" } inline fun forEachChild(block: (AssimpNode) -> Unit) { contract { callsInPlace(block) }; val array = children; for (i in 0 until numChildren) block(array[i]) } inline fun forEachMeshIndex(block: (Int) -> Unit) { contract { callsInPlace(block) }; val array = meshIndices; for (i in 0 until numMeshes) block(array[i]) } fun validate(): Unit = AINode.validate(ptr) } @JvmInline value class AssimpNodeArray(val ptr: Long) { val isNull get() = ptr == 0L val isNotNull get() = ptr != 0L operator fun get(index: Int) = AssimpNode(_U.getAddress(ptr + (index shl Pointer.POINTER_SHIFT))) } // Example usage from my project: private fun dumpNodeInfo(scene: AssimpScene, node: AssimpNode, outputPrefix: String, out: PrintStream) { out.print(outputPrefix) out.print(node.name.toString()) out.print(" (transform=") val t = node.readTransformation(Matrix4f()) out.printf("(%.2f, %.2f, %.2f, %.2f | %.2f, %.2f, %.2f, %.2f | %.2f, %.2f, %.2f, %.2f | %.2f, %.2f, %.2f, %.2f)", t.m00, t.m01, t.m02, t.m03, t.m10, t.m11, t.m12, t.m13, t.m20, t.m21, t.m22, t.m23, t.m30, t.m31, t.m32, t.m33) out.println(")") if (node.numMeshes > 0) { out.print(outputPrefix) out.println(" +meshes:") node.forEachMeshIndex { index -> out.print("$outputPrefix $index: ") val mesh = scene.meshes[index] out.print(if (mesh.isNull) "null" else mesh.name.toString()) out.println() } } node.forEachChild { child -> dumpNodeInfo(scene, child, "$outputPrefix ", out) } } ```