UnixFdPass.kt
src/jvmMain/kotlin/afield/graphics/texture/UnixFdPass.kt · 87 lines
UnixFdPass sends file descriptors to another process over a Unix domain socket with SCM_RIGHTS, which is how Field hands exported GPU memory to its Linux encoder helper.
It calls a separately built JNI helper (native/field-fdpass/), loaded through its own class
loader; UnixFdPass.available reports whether that helper is present. Linux only.
package afield.graphics.texture
import afield.app.diagnostics.logging.lprintln
import java.io.File
import java.lang.reflect.Method
import java.net.URLClassLoader
object UnixFdPass
Sends raw file descriptors to another process via SCM_RIGHTS over a Unix domain socket — the
mechanism VulkanExternalMemory's field-encode-linux handoff actually needs.
Superseded design note: the first cut tried /proc/<pid>/fd/<fd> duplication in the helper
(cheap, no native glue) — it works for pipes and eventfds but not for NVIDIA's
VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT export, which
turned out to be backed by /dev/nvidiactl (a real device-special file): reopening that path
performs a fresh open() against the driver, yielding an fd that's numerically valid but
unrelated to the original memory object — cuImportExternalMemory then fails with
CUDA_ERROR_UNKNOWN (999). It does not work for dma-bufs either, contrary to what an earlier
version of this note claimed: a dma-buf lives on an anonymous inode with no re-openable path, and
open("/proc/self/fd/<dma-buf>") fails with ENXIO — measured while choosing the external-browser
Linux transport, which is why that one also has to use SCM_RIGHTS. SCM_RIGHTS duplicates the
actual kernel struct file* regardless of what's behind it, so it's the correct fix. Java has no standard-library sendmsg/ancillary-data
support, hence a JNI shim (native/field-fdpass/) rather than a pure-JVM mechanism.
This wrapper deliberately declares no external fun itself and loads the real native class
(fieldfdpass.UnixFdPassNative, a standalone jar, not part of this Kotlin module) through a
fresh URLClassLoader via reflection, rather than a normal classpath reference. That isolation
is load-bearing, not style: a class with a JNI external fun visible on Field4's main jvmMain
classpath breaks the live box/REPL execution host (NoClassDefFoundError: ReplState, a
misleading error that blames a nonexistent package declaration) the instant any code path
references it — see the "field4-jni-repl-bug" memory note. Loading it through an isolated
classloader Field's own classpath scanning never walks avoids that interaction entirely.
Linux only.
object UnixFdPass {
val available: Boolean get()
The optional helper must be built before starting an external-memory session.
val available: Boolean get() = sendFdsMethod?.let {
runCatching { it.declaringClass.getMethod("closeFd", Int::class.javaPrimitiveType) }.isSuccess
} ?: false
private val sendFdsMethod: Method? = runCatching {
val jar = File("native/field-fdpass/build/fieldfdpass-native.jar")
if (!jar.canRead()) {
lprintln("error,field-stream", "fieldfdpass-native.jar not found at ${jar.path} — build it: (cd native/field-fdpass && cmake -B build && cmake --build build)")
return@runCatching null
}
val lib = File("native/field-fdpass/build/libfieldfdpass.so")
val loader = URLClassLoader(arrayOf(jar.toURI().toURL()), UnixFdPass::class.java.classLoader)
val cls = Class.forName("fieldfdpass.UnixFdPassNative", true, loader)
// Must run from *this* classloader's own bytecode (see UnixFdPassNative.loadLibrary's
// comment) — calling System.load() from Kotlin directly registers the library under the
// wrong classloader and every native call fails with UnsatisfiedLinkError despite the
// symbol being present in the process.
cls.getMethod("loadLibrary", String::class.java).invoke(null, lib.absolutePath)
cls.getMethod("sendFds", String::class.java, IntArray::class.java)
}.getOrElse {
lprintln("error,field-stream", "failed to load fieldfdpass-native.jar: ${it.message}")
null
}
fun closeFd(fd: Int)
Close an owned export descriptor after SCM_RIGHTS duplicated it, or on setup failure.
fun closeFd(fd: Int) {
checkNotNull(sendFdsMethod) { "Build native/field-fdpass before exporting descriptors" }
.declaringClass.getMethod("closeFd", Int::class.javaPrimitiveType).invoke(null, fd)
}
fun sendFds(socketPath: String, fds: IntArray): Boolean
Connect to the AF_UNIX SOCK_STREAM socket at socketPath (a peer must already be listening
— see field-encode-linux's Linux open handling) and send every fd in fds in one
sendmsg call (one SCM_RIGHTS control message, fds preserved in array order). Returns false
on any failure (socket missing/not listening yet, connect refused, send failed) — callers
should retry with backoff for the "not listening yet" race rather than fail immediately.
fun sendFds(socketPath: String, fds: IntArray): Boolean {
val method = sendFdsMethod ?: return false
return runCatching { method.invoke(null, socketPath, fds) as Boolean }
.getOrElse { lprintln("error,field-stream", "sendFds failed: ${it.message}"); false }
}
}