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.
|
* Sugar for [LyngClassBridge.bind] on a module scope.
|
||||||
*
|
*
|
||||||
@ -132,6 +177,16 @@ suspend fun ModuleScope.bind(
|
|||||||
block: ClassBridgeBinder.() -> Unit
|
block: ClassBridgeBinder.() -> Unit
|
||||||
): ObjClass = LyngClassBridge.bind(this, className, block)
|
): 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. */
|
/** Kotlin-side data slot attached to a Lyng instance. */
|
||||||
var ObjInstance.data: Any?
|
var ObjInstance.data: Any?
|
||||||
get() = kotlinInstanceData
|
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(
|
private suspend fun resolveClass(
|
||||||
className: String,
|
className: String,
|
||||||
module: String?,
|
module: String?,
|
||||||
@ -349,3 +612,26 @@ private suspend fun resolveClass(
|
|||||||
}
|
}
|
||||||
throw ScriptError(Pos.builtIn, "class $className not found in module ${scope.packageName}")
|
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
|
package net.sergeych.lyng
|
||||||
|
|
||||||
import kotlin.test.Test
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import net.sergeych.lyng.bridge.LyngClassBridge
|
import net.sergeych.lyng.bridge.LyngClassBridge
|
||||||
|
import net.sergeych.lyng.bridge.bindObject
|
||||||
import net.sergeych.lyng.bridge.data
|
import net.sergeych.lyng.bridge.data
|
||||||
import net.sergeych.lyng.obj.ObjInt
|
import net.sergeych.lyng.obj.ObjInt
|
||||||
import net.sergeych.lyng.obj.ObjString
|
import net.sergeych.lyng.obj.ObjString
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
class BridgeBindingTest {
|
class BridgeBindingTest {
|
||||||
private data class CounterState(var count: Long)
|
private data class CounterState(var count: Long)
|
||||||
@ -145,4 +162,55 @@ class BridgeBindingTest {
|
|||||||
}
|
}
|
||||||
assertTrue(bindFailed)
|
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