PdNative.kt
src/jvmMain/kotlin/afield/audio/pd/PdNative.kt · 301 lines
@file:afield.runtime.completion.CompletionCategory("Synthesis")
package afield.audio.pd
import java.lang.foreign.Arena
import java.lang.foreign.FunctionDescriptor
import java.lang.foreign.Linker
import java.lang.foreign.MemorySegment
import java.lang.foreign.SymbolLookup
import java.lang.foreign.ValueLayout.ADDRESS
import java.lang.foreign.ValueLayout.JAVA_FLOAT
import java.lang.foreign.ValueLayout.JAVA_INT
import java.lang.foreign.ValueLayout.JAVA_LONG
import java.lang.invoke.MethodHandle
import java.lang.invoke.MethodHandles
import java.lang.invoke.MethodType
import java.nio.file.Path
import kotlin.io.path.exists
Finds the optional libpd native library (PdLibrary) and binds its C functions for PdInstance.
object PdLibrary
Where libpd is, and what to do when it is not there.
libpd is an optional native dependency, like Manifold, Ceres and TetGen: a clone of Field
builds, tests and runs without it, and only the code that reaches for PdInstance needs it.
Nothing is committed; python3 tools/build_libpd_native.py (or ./gradlew buildLibpdNative)
builds one pinned libpd into build/field-libpd/.
Resolution order, first hit wins:
-Dafield.pd.library=<path>AFIELD_PD_LIBRARY=<path>build/field-libpd/build/libs/libpd-multi.<ext>relative to the working directory, which is the checkout forrunJvm,jvmTestand the backdoor
It must be the multi-instance build (PD_MULTI=ON, hence libpd-multi): PdInstance gives
every patch owner its own Pd, and a single-instance libpd would silently share one between them.
object PdLibrary {
private val os: String get() = System.getProperty("os.name").lowercase()
val fileName: String
The platform's file name for the multi-instance libpd.
val fileName: String
get() = when {
os.contains("win") -> "libpd-multi.dll"
os.contains("mac") || os.contains("darwin") -> "libpd-multi.dylib"
else -> "libpd-multi.so"
}
val searchedPaths: List<Path>
Every path resolvedPath tries, in order — quoted in the "not built" error.
val searchedPaths: List<Path>
get() = listOfNotNull(
System.getProperty("afield.pd.library")?.takeIf { it.isNotBlank() },
System.getenv("AFIELD_PD_LIBRARY")?.takeIf { it.isNotBlank() },
).map(Path::of) + listOf(
Path.of("build", "field-libpd", "build", "libs", fileName).toAbsolutePath(),
Path.of("build", "field-libpd", "build", "libs", "Release", fileName).toAbsolutePath(),
)
val resolvedPath: Path? get() = searchedPaths.firstOrNull { it.exists() }
Not cached: building it during a live session should be picked up by the next box run.
val isAvailable: Boolean get() = resolvedPath != null
True when a libpd is where searchedPaths looks.
val HOW_TO_BUILD: String
The one instruction a missing library needs.
val HOW_TO_BUILD: String
get() = "Build it with `python3 tools/build_libpd_native.py` or `./gradlew buildLibpdNative` " +
"(needs cmake, git and the network once), or point -Dafield.pd.library at a $fileName you built."
fun describe(): String
One line for a box or a test to print.
fun describe(): String = resolvedPath?.let { "libpd at $it" }
?: "libpd not built. $HOW_TO_BUILD"
}
internal object PdNative
The C ABI of z_libpd.h, bound with Java FFM. No wrapper library of Field's own sits between.
Nothing here is thread-safe, and nothing here knows which Pd it is talking to: in a PD_MULTI
build the current instance is a thread-local inside libpd, set by setInstance. PdInstance
is the only caller, and it always takes its own lock and sets itself current before any other call.
declaration — 78 lines, from line 86
internal object PdNative {
private val linker: Linker = Linker.nativeLinker()
private val lookup: SymbolLookup by lazy {
val path = PdLibrary.resolvedPath
?: throw UnsatisfiedLinkError(
"libpd is not built. ${PdLibrary.HOW_TO_BUILD}\nLooked at: " +
PdLibrary.searchedPaths.joinToString(", "),
)
SymbolLookup.libraryLookup(path, Arena.global())
}
private fun handle(name: String, descriptor: FunctionDescriptor): MethodHandle {
val symbol = lookup.find(name).orElseThrow {
UnsatisfiedLinkError("${PdLibrary.fileName} has no symbol $name; rebuild it with tools/build_libpd_native.py")
}
return linker.downcallHandle(symbol, descriptor)
}
private fun v(vararg args: java.lang.foreign.MemoryLayout) = FunctionDescriptor.ofVoid(*args)
private fun i(vararg args: java.lang.foreign.MemoryLayout) = FunctionDescriptor.of(JAVA_INT, *args)
private fun p(vararg args: java.lang.foreign.MemoryLayout) = FunctionDescriptor.of(ADDRESS, *args)
private val initH by lazy { handle("libpd_init", i()) }
private val newInstanceH by lazy { handle("libpd_new_instance", p()) }
private val setInstanceH by lazy { handle("libpd_set_instance", v(ADDRESS)) }
private val freeInstanceH by lazy { handle("libpd_free_instance", v(ADDRESS)) }
private val numInstancesH by lazy { handle("libpd_num_instances", i()) }
private val initAudioH by lazy { handle("libpd_init_audio", i(JAVA_INT, JAVA_INT, JAVA_INT)) }
private val blockSizeH by lazy { handle("libpd_blocksize", i()) }
private val addToSearchPathH by lazy { handle("libpd_add_to_search_path", v(ADDRESS)) }
private val openFileH by lazy { handle("libpd_openfile", p(ADDRESS, ADDRESS)) }
private val closeFileH by lazy { handle("libpd_closefile", v(ADDRESS)) }
private val dollarZeroH by lazy { handle("libpd_getdollarzero", i(ADDRESS)) }
private val processFloatH by lazy { handle("libpd_process_float", i(JAVA_INT, ADDRESS, ADDRESS)) }
private val bangH by lazy { handle("libpd_bang", i(ADDRESS)) }
private val floatH by lazy { handle("libpd_float", i(ADDRESS, JAVA_FLOAT)) }
private val symbolH by lazy { handle("libpd_symbol", i(ADDRESS, ADDRESS)) }
private val startMessageH by lazy { handle("libpd_start_message", i(JAVA_INT)) }
private val addFloatH by lazy { handle("libpd_add_float", v(JAVA_FLOAT)) }
private val addSymbolH by lazy { handle("libpd_add_symbol", v(ADDRESS)) }
private val finishListH by lazy { handle("libpd_finish_list", i(ADDRESS)) }
private val finishMessageH by lazy { handle("libpd_finish_message", i(ADDRESS, ADDRESS)) }
private val bindH by lazy { handle("libpd_bind", p(ADDRESS)) }
private val unbindH by lazy { handle("libpd_unbind", v(ADDRESS)) }
private val existsH by lazy { handle("libpd_exists", i(ADDRESS)) }
private val arraySizeH by lazy { handle("libpd_arraysize", i(ADDRESS)) }
private val resizeArrayH by lazy { handle("libpd_resize_array", i(ADDRESS, JAVA_LONG)) }
private val readArrayH by lazy { handle("libpd_read_array", i(ADDRESS, ADDRESS, JAVA_INT, JAVA_INT)) }
private val writeArrayH by lazy { handle("libpd_write_array", i(ADDRESS, JAVA_INT, ADDRESS, JAVA_INT)) }
private val isFloatH by lazy { handle("libpd_is_float", i(ADDRESS)) }
private val isSymbolH by lazy { handle("libpd_is_symbol", i(ADDRESS)) }
private val getFloatH by lazy { handle("libpd_get_float", FunctionDescriptor.of(JAVA_FLOAT, ADDRESS)) }
private val getSymbolH by lazy { handle("libpd_get_symbol", p(ADDRESS)) }
private val nextAtomH by lazy { handle("libpd_next_atom", p(ADDRESS)) }
private val noteOnH by lazy { handle("libpd_noteon", i(JAVA_INT, JAVA_INT, JAVA_INT)) }
private val controlChangeH by lazy { handle("libpd_controlchange", i(JAVA_INT, JAVA_INT, JAVA_INT)) }
private val programChangeH by lazy { handle("libpd_programchange", i(JAVA_INT, JAVA_INT)) }
private val pitchBendH by lazy { handle("libpd_pitchbend", i(JAVA_INT, JAVA_INT)) }
private val setNoteOnHookH by lazy { handle("libpd_set_noteonhook", v(ADDRESS)) }
private val setControlChangeHookH by lazy { handle("libpd_set_controlchangehook", v(ADDRESS)) }
private val setProgramChangeHookH by lazy { handle("libpd_set_programchangehook", v(ADDRESS)) }
private val setPitchBendHookH by lazy { handle("libpd_set_pitchbendhook", v(ADDRESS)) }
private val setPrintHookH by lazy { handle("libpd_set_printhook", v(ADDRESS)) }
private val setBangHookH by lazy { handle("libpd_set_banghook", v(ADDRESS)) }
private val setFloatHookH by lazy { handle("libpd_set_floathook", v(ADDRESS)) }
private val setSymbolHookH by lazy { handle("libpd_set_symbolhook", v(ADDRESS)) }
private val setListHookH by lazy { handle("libpd_set_listhook", v(ADDRESS)) }
private val setMessageHookH by lazy { handle("libpd_set_messagehook", v(ADDRESS)) }
private val DISCARD: MethodHandle = MethodHandles.lookup().findStatic(
PdNative::class.java, "discard", MethodType.methodType(Void.TYPE, MemorySegment::class.java))
@JvmStatic
private fun discard(@Suppress("UNUSED_PARAMETER") s: MemorySegment) {}
libpd_init may be called once per process, and before any instance exists.
private var initialised = false
fun ensureInitialised()
@Synchronized
fun ensureInitialised() {
if (initialised) return
// Before init this sets the process-wide default print hook. Without it libpd posts its
// version and the banner of every `extra` object to stdout once per JVM; each instance
// installs its own hook afterwards, so nothing a patch prints is lost.
setPrintHook(linker.upcallStub(DISCARD, v(ADDRESS), Arena.global()))
initH.invokeExact() as Int
initialised = true
}
fun newInstance(): MemorySegment = newInstanceH.invokeExact() as MemorySegment
fun setInstance(instance: MemorySegment) { setInstanceH.invokeExact(instance) }
fun freeInstance(instance: MemorySegment) { freeInstanceH.invokeExact(instance) }
fun numInstances(): Int = numInstancesH.invokeExact() as Int
fun initAudio(inputs: Int, outputs: Int, sampleRate: Int): Int
fun initAudio(inputs: Int, outputs: Int, sampleRate: Int): Int =
initAudioH.invokeExact(inputs, outputs, sampleRate) as Int
fun blockSize(): Int = blockSizeH.invokeExact() as Int
fun addToSearchPath(path: MemorySegment) { addToSearchPathH.invokeExact(path) }
fun openFile(name: MemorySegment, dir: MemorySegment): MemorySegment
fun openFile(name: MemorySegment, dir: MemorySegment): MemorySegment =
openFileH.invokeExact(name, dir) as MemorySegment
fun closeFile(patch: MemorySegment) { closeFileH.invokeExact(patch) }
fun dollarZero(patch: MemorySegment): Int = dollarZeroH.invokeExact(patch) as Int
fun processFloat(ticks: Int, input: MemorySegment, output: MemorySegment): Int
fun processFloat(ticks: Int, input: MemorySegment, output: MemorySegment): Int =
processFloatH.invokeExact(ticks, input, output) as Int
fun bang(receiver: MemorySegment): Int = bangH.invokeExact(receiver) as Int
fun float(receiver: MemorySegment, x: Float): Int = floatH.invokeExact(receiver, x) as Int
fun symbol(receiver: MemorySegment, s: MemorySegment): Int
fun symbol(receiver: MemorySegment, s: MemorySegment): Int = symbolH.invokeExact(receiver, s) as Int
fun startMessage(max: Int): Int = startMessageH.invokeExact(max) as Int
fun addFloat(x: Float) { addFloatH.invokeExact(x) }
fun addSymbol(s: MemorySegment) { addSymbolH.invokeExact(s) }
fun finishList(receiver: MemorySegment): Int = finishListH.invokeExact(receiver) as Int
fun finishMessage(receiver: MemorySegment, selector: MemorySegment): Int
fun finishMessage(receiver: MemorySegment, selector: MemorySegment): Int =
finishMessageH.invokeExact(receiver, selector) as Int
fun bind(name: MemorySegment): MemorySegment = bindH.invokeExact(name) as MemorySegment
fun unbind(binding: MemorySegment) { unbindH.invokeExact(binding) }
fun exists(name: MemorySegment): Int = existsH.invokeExact(name) as Int
fun arraySize(name: MemorySegment): Int = arraySizeH.invokeExact(name) as Int
fun resizeArray(name: MemorySegment, size: Long): Int
fun resizeArray(name: MemorySegment, size: Long): Int = resizeArrayH.invokeExact(name, size) as Int
fun readArray(dest: MemorySegment, name: MemorySegment, offset: Int, n: Int): Int
fun readArray(dest: MemorySegment, name: MemorySegment, offset: Int, n: Int): Int =
readArrayH.invokeExact(dest, name, offset, n) as Int
fun writeArray(name: MemorySegment, offset: Int, src: MemorySegment, n: Int): Int
fun writeArray(name: MemorySegment, offset: Int, src: MemorySegment, n: Int): Int =
writeArrayH.invokeExact(name, offset, src, n) as Int
fun noteOn(channel: Int, pitch: Int, velocity: Int): Int
fun noteOn(channel: Int, pitch: Int, velocity: Int): Int = noteOnH.invokeExact(channel, pitch, velocity) as Int
fun controlChange(channel: Int, controller: Int, value: Int): Int
fun controlChange(channel: Int, controller: Int, value: Int): Int =
controlChangeH.invokeExact(channel, controller, value) as Int
fun programChange(channel: Int, value: Int): Int
fun programChange(channel: Int, value: Int): Int = programChangeH.invokeExact(channel, value) as Int
fun pitchBend(channel: Int, value: Int): Int = pitchBendH.invokeExact(channel, value) as Int
fun setPrintHook(fn: MemorySegment) { setPrintHookH.invokeExact(fn) }
fun setBangHook(fn: MemorySegment) { setBangHookH.invokeExact(fn) }
fun setFloatHook(fn: MemorySegment) { setFloatHookH.invokeExact(fn) }
fun setSymbolHook(fn: MemorySegment) { setSymbolHookH.invokeExact(fn) }
fun setListHook(fn: MemorySegment) { setListHookH.invokeExact(fn) }
fun setMessageHook(fn: MemorySegment) { setMessageHookH.invokeExact(fn) }
fun atoms(argc: Int, argv: MemorySegment): List<Any>
Decode argc atoms starting at argv into Kotlin values: Float or String.
argv arrives as a zero-length segment from an upcall; the atoms are walked with libpd's
own accessors rather than by assuming sizeof(t_atom), which differs by platform.
fun atoms(argc: Int, argv: MemorySegment): List<Any> {
if (argc <= 0 || argv == MemorySegment.NULL) return emptyList()
val out = ArrayList<Any>(argc)
var atom = argv
for (k in 0 until argc) {
when {
(isFloatH.invokeExact(atom) as Int) != 0 -> out += (getFloatH.invokeExact(atom) as Float)
(isSymbolH.invokeExact(atom) as Int) != 0 ->
out += cString(getSymbolH.invokeExact(atom) as MemorySegment)
else -> out += "?"
}
if (k < argc - 1) atom = nextAtomH.invokeExact(atom) as MemorySegment
}
return out
}
fun cString(segment: MemorySegment): String
Read a NUL-terminated C string handed over by libpd.
fun cString(segment: MemorySegment): String =
if (segment == MemorySegment.NULL) "" else segment.reinterpret(Long.MAX_VALUE).getString(0)
upcalls
interface Hooks
What a PdInstance implements to receive libpd's hooks. Must never throw.
fun print(s: MemorySegment)
fun bang(recv: MemorySegment)
fun float(recv: MemorySegment, x: Float)
fun symbol(recv: MemorySegment, s: MemorySegment)
fun list(recv: MemorySegment, argc: Int, argv: MemorySegment)
fun message(recv: MemorySegment, msg: MemorySegment, argc: Int, argv: MemorySegment)
fun noteOn(channel: Int, pitch: Int, velocity: Int)
fun controlChange(channel: Int, controller: Int, value: Int)
fun programChange(channel: Int, value: Int)
fun pitchBend(channel: Int, value: Int)
private val lookupHooks = MethodHandles.lookup()
private val seg = MemorySegment::class.java
private val voidT = Void.TYPE
private val intT = Int::class.javaPrimitiveType!!
private val floatT = Float::class.javaPrimitiveType!!
private fun target(name: String, type: MethodType): MethodHandle =
lookupHooks.findVirtual(Hooks::class.java, name, type)
private val PRINT = target("print", MethodType.methodType(voidT, seg))
private val BANG = target("bang", MethodType.methodType(voidT, seg))
private val FLOAT = target("float", MethodType.methodType(voidT, seg, floatT))
private val SYMBOL = target("symbol", MethodType.methodType(voidT, seg, seg))
private val LIST = target("list", MethodType.methodType(voidT, seg, intT, seg))
private val MESSAGE = target("message", MethodType.methodType(voidT, seg, seg, intT, seg))
private val NOTE_ON = target("noteOn", MethodType.methodType(voidT, intT, intT, intT))
private val CONTROL_CHANGE = target("controlChange", MethodType.methodType(voidT, intT, intT, intT))
private val PROGRAM_CHANGE = target("programChange", MethodType.methodType(voidT, intT, intT))
private val PITCH_BEND = target("pitchBend", MethodType.methodType(voidT, intT, intT))
fun installHooks(hooks: Hooks, arena: Arena)
Install every hook for the current instance, as upcall stubs that live as long as arena.
fun installHooks(hooks: Hooks, arena: Arena) {
setPrintHook(linker.upcallStub(PRINT.bindTo(hooks), v(ADDRESS), arena))
setBangHook(linker.upcallStub(BANG.bindTo(hooks), v(ADDRESS), arena))
setFloatHook(linker.upcallStub(FLOAT.bindTo(hooks), v(ADDRESS, JAVA_FLOAT), arena))
setSymbolHook(linker.upcallStub(SYMBOL.bindTo(hooks), v(ADDRESS, ADDRESS), arena))
setListHook(linker.upcallStub(LIST.bindTo(hooks), v(ADDRESS, JAVA_INT, ADDRESS), arena))
setMessageHook(linker.upcallStub(MESSAGE.bindTo(hooks), v(ADDRESS, ADDRESS, JAVA_INT, ADDRESS), arena))
setNoteOnHookH.invokeExact(linker.upcallStub(NOTE_ON.bindTo(hooks), v(JAVA_INT, JAVA_INT, JAVA_INT), arena))
setControlChangeHookH.invokeExact(linker.upcallStub(CONTROL_CHANGE.bindTo(hooks), v(JAVA_INT, JAVA_INT, JAVA_INT), arena))
setProgramChangeHookH.invokeExact(linker.upcallStub(PROGRAM_CHANGE.bindTo(hooks), v(JAVA_INT, JAVA_INT), arena))
setPitchBendHookH.invokeExact(linker.upcallStub(PITCH_BEND.bindTo(hooks), v(JAVA_INT, JAVA_INT), arena))
}
}