LWJGL / lwjgl3

LWJGL is a Java library that enables cross-platform access to popular native APIs useful in the development of graphics (OpenGL, Vulkan, bgfx), audio (OpenAL, Opus), parallel computing (OpenCL, CUDA) and XR (OpenVR, LibOVR, OpenXR) applications.
https://www.lwjgl.org
BSD 3-Clause "New" or "Revised" License
4.67k stars 628 forks source link

Extend most JNI "pointers" by wrapping with Kotlin @JvmInline value classes #872

Closed philipguin closed 1 year ago

philipguin commented 1 year ago

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:

object MyBindings {
    external fun createObj(): Long
    external fun getFoo(pObj: Long): Int
    external fun setFoo(pObj: Long, foo: Int)
}
fun demo() {
    val pObj = MyBindings.createObject()
    val foo = MyBindings.getFoo(pObj)
    bindings.setFoo(pObj, foo + 1)
}

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:

@JvmInline value class Obj(val ptr: Long) {
    val isNull: Boolean get() = ptr == 0L
    val isNotNull: Boolean get() = ptr != 0L

    val foo: Int
        get() = MyBindings.getFoo(ptr)
        set(v) { MyBindings.setFoo(ptr, v) }

    companion object {
        const val SIZEOF = 32
        fun alloc(): Obj = Obj(MyBindings.createObj())
    }
}
// Equivalent to fun demo:
fun demo2() {
    val obj = Obj.alloc()
    obj.foo += 1
}

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) } } ```
Spasi commented 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.

philipguin commented 1 year ago

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!