Kotlin bride: object bindings added
This commit is contained in:
parent
45f3658742
commit
e7c1adb2c5
@ -122,6 +122,51 @@ object LyngClassBridge {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry point for Kotlin bindings to declared Lyng objects (singleton instances).
|
||||
*
|
||||
* Works similarly to [LyngClassBridge], but targets an already created object instance.
|
||||
*/
|
||||
object LyngObjectBridge {
|
||||
/**
|
||||
* Resolve a Lyng object by [objectName] and bind Kotlin implementations.
|
||||
*
|
||||
* @param module module name used for resolution (required when [module] scope is not provided)
|
||||
* @param importManager import manager used to resolve the module
|
||||
*/
|
||||
suspend fun bind(
|
||||
objectName: String,
|
||||
module: String? = null,
|
||||
importManager: ImportManager = Script.defaultImportManager,
|
||||
block: ClassBridgeBinder.() -> Unit
|
||||
): ObjInstance {
|
||||
val obj = resolveObject(objectName, module, null, importManager)
|
||||
return bind(obj, block)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a Lyng object within an existing [moduleScope] and bind Kotlin implementations.
|
||||
*/
|
||||
suspend fun bind(
|
||||
moduleScope: ModuleScope,
|
||||
objectName: String,
|
||||
block: ClassBridgeBinder.() -> Unit
|
||||
): ObjInstance {
|
||||
val obj = resolveObject(objectName, null, moduleScope, Script.defaultImportManager)
|
||||
return bind(obj, block)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind Kotlin implementations directly to an already resolved object [instance].
|
||||
*/
|
||||
suspend fun bind(instance: ObjInstance, block: ClassBridgeBinder.() -> Unit): ObjInstance {
|
||||
val binder = ObjectBridgeBinderImpl(instance)
|
||||
binder.block()
|
||||
binder.commit()
|
||||
return instance
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sugar for [LyngClassBridge.bind] on a module scope.
|
||||
*
|
||||
@ -132,6 +177,16 @@ suspend fun ModuleScope.bind(
|
||||
block: ClassBridgeBinder.() -> Unit
|
||||
): ObjClass = LyngClassBridge.bind(this, className, block)
|
||||
|
||||
/**
|
||||
* Sugar for [LyngObjectBridge.bind] on a module scope.
|
||||
*
|
||||
* Bound members must be declared as `extern` in Lyng.
|
||||
*/
|
||||
suspend fun ModuleScope.bindObject(
|
||||
objectName: String,
|
||||
block: ClassBridgeBinder.() -> Unit
|
||||
): ObjInstance = LyngObjectBridge.bind(this, objectName, block)
|
||||
|
||||
/** Kotlin-side data slot attached to a Lyng instance. */
|
||||
var ObjInstance.data: Any?
|
||||
get() = kotlinInstanceData
|
||||
@ -327,6 +382,214 @@ private class ClassBridgeBinderImpl(
|
||||
}
|
||||
}
|
||||
|
||||
private class ObjectBridgeBinderImpl(
|
||||
private val instance: ObjInstance
|
||||
) : ClassBridgeBinder {
|
||||
private val cls: ObjClass = instance.objClass
|
||||
private val initHooks = mutableListOf<suspend (ScopeFacade, ObjInstance) -> Unit>()
|
||||
|
||||
override var classData: Any?
|
||||
get() = cls.kotlinClassData
|
||||
set(value) { cls.kotlinClassData = value }
|
||||
|
||||
override fun init(block: suspend BridgeInstanceContext.(ScopeFacade) -> Unit) {
|
||||
initHooks.add { scope, inst ->
|
||||
val ctx = BridgeInstanceContextImpl(inst)
|
||||
ctx.block(scope)
|
||||
}
|
||||
}
|
||||
|
||||
override fun initWithInstance(block: suspend (ScopeFacade, Obj) -> Unit) {
|
||||
initHooks.add { scope, inst ->
|
||||
block(scope, inst)
|
||||
}
|
||||
}
|
||||
|
||||
override fun addFun(name: String, impl: suspend (ScopeFacade, Obj, Arguments) -> Obj) {
|
||||
val target = findMember(name)
|
||||
val callable = ObjExternCallable.fromBridge {
|
||||
impl(this, thisObj, args)
|
||||
}
|
||||
val methodId = cls.ensureMethodIdForBridge(name, target.record)
|
||||
val newRecord = target.record.copy(
|
||||
value = callable,
|
||||
type = ObjRecord.Type.Fun,
|
||||
methodId = methodId
|
||||
)
|
||||
replaceMember(target, newRecord)
|
||||
updateInstanceMember(target, newRecord)
|
||||
}
|
||||
|
||||
override fun addVal(name: String, impl: suspend (ScopeFacade, Obj) -> Obj) {
|
||||
val target = findMember(name)
|
||||
if (target.record.isMutable) {
|
||||
throw ScriptError(Pos.builtIn, "extern val $name is mutable in class ${cls.className}")
|
||||
}
|
||||
val getter = ObjExternCallable.fromBridge {
|
||||
impl(this, thisObj)
|
||||
}
|
||||
val prop = ObjProperty(name, getter, null)
|
||||
val isFieldLike = target.record.type == ObjRecord.Type.Field ||
|
||||
target.record.type == ObjRecord.Type.ConstructorField
|
||||
val newRecord = if (isFieldLike) {
|
||||
removeFieldInitializersFor(name)
|
||||
target.record.copy(
|
||||
value = prop,
|
||||
type = target.record.type,
|
||||
fieldId = target.record.fieldId,
|
||||
methodId = target.record.methodId
|
||||
)
|
||||
} else {
|
||||
val methodId = cls.ensureMethodIdForBridge(name, target.record)
|
||||
target.record.copy(
|
||||
value = prop,
|
||||
type = ObjRecord.Type.Property,
|
||||
methodId = methodId,
|
||||
fieldId = null
|
||||
)
|
||||
}
|
||||
replaceMember(target, newRecord)
|
||||
updateInstanceMember(target, newRecord)
|
||||
}
|
||||
|
||||
override fun addVar(
|
||||
name: String,
|
||||
get: suspend (ScopeFacade, Obj) -> Obj,
|
||||
set: suspend (ScopeFacade, Obj, Obj) -> Unit
|
||||
) {
|
||||
val target = findMember(name)
|
||||
if (!target.record.isMutable) {
|
||||
throw ScriptError(Pos.builtIn, "extern var $name is readonly in class ${cls.className}")
|
||||
}
|
||||
val getter = ObjExternCallable.fromBridge {
|
||||
get(this, thisObj)
|
||||
}
|
||||
val setter = ObjExternCallable.fromBridge {
|
||||
val value = requiredArg<Obj>(0)
|
||||
set(this, thisObj, value)
|
||||
ObjVoid
|
||||
}
|
||||
val prop = ObjProperty(name, getter, setter)
|
||||
val isFieldLike = target.record.type == ObjRecord.Type.Field ||
|
||||
target.record.type == ObjRecord.Type.ConstructorField
|
||||
val newRecord = if (isFieldLike) {
|
||||
removeFieldInitializersFor(name)
|
||||
target.record.copy(
|
||||
value = prop,
|
||||
type = target.record.type,
|
||||
fieldId = target.record.fieldId,
|
||||
methodId = target.record.methodId
|
||||
)
|
||||
} else {
|
||||
val methodId = cls.ensureMethodIdForBridge(name, target.record)
|
||||
target.record.copy(
|
||||
value = prop,
|
||||
type = ObjRecord.Type.Property,
|
||||
methodId = methodId,
|
||||
fieldId = null
|
||||
)
|
||||
}
|
||||
replaceMember(target, newRecord)
|
||||
updateInstanceMember(target, newRecord)
|
||||
}
|
||||
|
||||
suspend fun commit() {
|
||||
if (initHooks.isNotEmpty()) {
|
||||
val target = cls.bridgeInitHooks ?: mutableListOf<suspend (ScopeFacade, ObjInstance) -> Unit>().also {
|
||||
cls.bridgeInitHooks = it
|
||||
}
|
||||
target.addAll(initHooks)
|
||||
val facade = instance.instanceScope.asFacade()
|
||||
for (hook in initHooks) {
|
||||
hook(facade, instance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun replaceMember(target: MemberTarget, newRecord: ObjRecord) {
|
||||
when (target.kind) {
|
||||
MemberKind.Instance -> {
|
||||
cls.replaceMemberForBridge(target.name, newRecord)
|
||||
if (target.mirrorClassScope && cls.classScope?.objects?.containsKey(target.name) == true) {
|
||||
cls.replaceClassScopeMemberForBridge(target.name, newRecord)
|
||||
}
|
||||
}
|
||||
MemberKind.Static -> cls.replaceClassScopeMemberForBridge(target.name, newRecord)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateInstanceMember(target: MemberTarget, newRecord: ObjRecord) {
|
||||
val key = instanceStorageKey(target, newRecord) ?: return
|
||||
ensureInstanceSlotCapacity()
|
||||
instance.instanceScope.objects[key] = newRecord
|
||||
cls.fieldSlotForKey(key)?.let { slot ->
|
||||
instance.setFieldSlotRecord(slot.slot, newRecord)
|
||||
}
|
||||
cls.methodSlotForKey(key)?.let { slot ->
|
||||
instance.setMethodSlotRecord(slot.slot, newRecord)
|
||||
}
|
||||
}
|
||||
|
||||
private fun instanceStorageKey(target: MemberTarget, rec: ObjRecord): String? = when (target.kind) {
|
||||
MemberKind.Instance -> {
|
||||
if (rec.visibility == Visibility.Private ||
|
||||
rec.type == ObjRecord.Type.Field ||
|
||||
rec.type == ObjRecord.Type.ConstructorField ||
|
||||
rec.type == ObjRecord.Type.Delegated) {
|
||||
cls.mangledName(target.name)
|
||||
} else {
|
||||
target.name
|
||||
}
|
||||
}
|
||||
MemberKind.Static -> {
|
||||
if (rec.type != ObjRecord.Type.Fun &&
|
||||
rec.type != ObjRecord.Type.Property &&
|
||||
rec.type != ObjRecord.Type.Delegated) {
|
||||
null
|
||||
} else if (rec.visibility == Visibility.Private || rec.type == ObjRecord.Type.Delegated) {
|
||||
cls.mangledName(target.name)
|
||||
} else {
|
||||
target.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureInstanceSlotCapacity() {
|
||||
val fieldCount = cls.fieldSlotCount()
|
||||
if (instance.fieldSlots.size < fieldCount) {
|
||||
val newSlots = arrayOfNulls<ObjRecord>(fieldCount)
|
||||
instance.fieldSlots.copyInto(newSlots, 0, 0, instance.fieldSlots.size)
|
||||
instance.fieldSlots = newSlots
|
||||
}
|
||||
val methodCount = cls.methodSlotCount()
|
||||
if (instance.methodSlots.size < methodCount) {
|
||||
val newSlots = arrayOfNulls<ObjRecord>(methodCount)
|
||||
instance.methodSlots.copyInto(newSlots, 0, 0, instance.methodSlots.size)
|
||||
instance.methodSlots = newSlots
|
||||
}
|
||||
}
|
||||
|
||||
private fun findMember(name: String): MemberTarget {
|
||||
val inst = cls.members[name]
|
||||
val stat = cls.classScope?.objects?.get(name)
|
||||
if (inst != null) {
|
||||
return MemberTarget(name, inst, MemberKind.Instance, mirrorClassScope = stat != null)
|
||||
}
|
||||
if (stat != null) return MemberTarget(name, stat, MemberKind.Static)
|
||||
throw ScriptError(Pos.builtIn, "extern member $name not found in class ${cls.className}")
|
||||
}
|
||||
|
||||
private fun removeFieldInitializersFor(name: String) {
|
||||
if (cls.instanceInitializers.isEmpty()) return
|
||||
val storageName = cls.mangledName(name)
|
||||
cls.instanceInitializers.removeAll { init ->
|
||||
val stmt = init as? Statement ?: return@removeAll false
|
||||
val original = (stmt as? BytecodeStatement)?.original ?: stmt
|
||||
original is InstanceFieldInitStatement && original.storageName == storageName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun resolveClass(
|
||||
className: String,
|
||||
module: String?,
|
||||
@ -349,3 +612,26 @@ private suspend fun resolveClass(
|
||||
}
|
||||
throw ScriptError(Pos.builtIn, "class $className not found in module ${scope.packageName}")
|
||||
}
|
||||
|
||||
private suspend fun resolveObject(
|
||||
objectName: String,
|
||||
module: String?,
|
||||
moduleScope: ModuleScope?,
|
||||
importManager: ImportManager
|
||||
): ObjInstance {
|
||||
val scope = moduleScope ?: run {
|
||||
if (module == null) {
|
||||
throw ScriptError(Pos.builtIn, "module is required to resolve $objectName")
|
||||
}
|
||||
importManager.createModuleScope(Pos.builtIn, module)
|
||||
}
|
||||
val rec = scope.get(objectName)
|
||||
val direct = rec?.value as? ObjInstance
|
||||
if (direct != null) return direct
|
||||
if (objectName.contains('.')) {
|
||||
val resolved = scope.resolveQualifiedIdentifier(objectName)
|
||||
val inst = resolved as? ObjInstance
|
||||
if (inst != null) return inst
|
||||
}
|
||||
throw ScriptError(Pos.builtIn, "object $objectName not found in module ${scope.packageName}")
|
||||
}
|
||||
|
||||
@ -1,13 +1,30 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.lyng.bridge.LyngClassBridge
|
||||
import net.sergeych.lyng.bridge.bindObject
|
||||
import net.sergeych.lyng.bridge.data
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import net.sergeych.lyng.obj.ObjString
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class BridgeBindingTest {
|
||||
private data class CounterState(var count: Long)
|
||||
@ -145,4 +162,55 @@ class BridgeBindingTest {
|
||||
}
|
||||
assertTrue(bindFailed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExternObjectBinding() = runTest {
|
||||
val im = Script.defaultImportManager.copy()
|
||||
im.addPackage("bridge.obj") { scope ->
|
||||
scope.eval(
|
||||
"""
|
||||
extern object HostObject {
|
||||
extern fun add(a: Int, b: Int): Int
|
||||
extern val status: String
|
||||
extern var count: Int
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
scope.bindObject("HostObject") {
|
||||
classData = "OK"
|
||||
init { _ ->
|
||||
data = CounterState(5)
|
||||
}
|
||||
addFun("add") { _, _, args ->
|
||||
val a = (args.list[0] as ObjInt).value
|
||||
val b = (args.list[1] as ObjInt).value
|
||||
ObjInt.of(a + b)
|
||||
}
|
||||
addVal("status") { _, _ -> ObjString(classData as String) }
|
||||
addVar(
|
||||
"count",
|
||||
get = { _, instance ->
|
||||
val st = (instance as net.sergeych.lyng.obj.ObjInstance).data as CounterState
|
||||
ObjInt.of(st.count)
|
||||
},
|
||||
set = { _, instance, value ->
|
||||
val st = (instance as net.sergeych.lyng.obj.ObjInstance).data as CounterState
|
||||
st.count = (value as ObjInt).value
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val scope = im.newStdScope()
|
||||
scope.eval(
|
||||
"""
|
||||
import bridge.obj
|
||||
assertEquals(42, HostObject.add(10, 32))
|
||||
assertEquals("OK", HostObject.status)
|
||||
assertEquals(5, HostObject.count)
|
||||
HostObject.count = HostObject.count + 1
|
||||
assertEquals(6, HostObject.count)
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user