Skip to content

Commit

Permalink
feat(coroutine): Add LambdaCallable::onCancel to cancel coroutine whe…
Browse files Browse the repository at this point in the history
…n callable is cancelled
  • Loading branch information
piiertho committed Feb 14, 2025
1 parent fcecb9f commit ce8f70f
Show file tree
Hide file tree
Showing 17 changed files with 563 additions and 357 deletions.
7 changes: 5 additions & 2 deletions harness/tests/scripts/godot/tests/coroutine/CoroutineTest.gdj
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ signals = [
async_load_resource_finished
]
properties = [
step
step,
was_child_cancelled,
was_parent_cancelled
]
functions = [
start_coroutine_without_parameter,
Expand All @@ -30,5 +32,6 @@ functions = [
start_coroutine_with_physics_frame,
start_coroutine_with_process_frame,
run_on_main_thread_from_background_thread,
async_load_resource
async_load_resource,
cancel_coroutine
]
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package godot.tests.coroutine
import godot.api.Object
import godot.api.PackedScene
import godot.api.ResourceLoader
import godot.api.Timer
import godot.annotation.RegisterClass
import godot.annotation.RegisterFunction
import godot.annotation.RegisterProperty
Expand All @@ -20,6 +21,8 @@ import godot.coroutines.awaitProcessFrame
import godot.coroutines.godotCoroutine
import godot.global.GD
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.async
import kotlinx.coroutines.delay

@RegisterClass
class CoroutineTest : Object() {
Expand All @@ -36,6 +39,12 @@ class CoroutineTest : Object() {
@RegisterProperty
var step: Int = 0

@RegisterProperty
var wasChildCancelled = false

@RegisterProperty
var wasParentCancelled = true

@RegisterFunction
fun startCoroutineWithoutParameter() = godotCoroutine {
step = 1
Expand Down Expand Up @@ -126,4 +135,19 @@ class CoroutineTest : Object() {
}
}
}

@RegisterFunction
fun cancelCoroutine() = godotCoroutine {
val timer = Timer()
timer.autostart = true
val job = async {
timer.start(3.0)
timer.timeout.await()
}
delay(1000)
timer.queueFree()
delay(1000)
wasChildCancelled = job.isCancelled
wasParentCancelled = false
}
}
5 changes: 5 additions & 0 deletions harness/tests/test/unit/test_coroutines.gd
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ func test_coroutine_await():
test_script.async_load_resource()
var async_load_resource_success = await test_script.async_load_resource_finished
assert_true(async_load_resource_success, "Resource should be loaded")

test_script.cancel_coroutine()
await get_tree().create_timer(4).timeout
assert_true(test_script.was_child_cancelled, "Coroutine children should have been cancelled")
assert_false(test_script.was_parent_cancelled, "Coroutine parent should not have been cancelled")

await get_tree().create_timer(1).timeout
test_script.free()
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.UNIT
import godot.codegen.services.IAwaitGenerationService
import godot.common.constants.Constraints
import godot.tools.common.constants.AS_CALLABLE_UTIL_FUNCTION
import godot.tools.common.constants.GODOT_OBJECT
import godot.tools.common.constants.GodotKotlinJvmTypes.signal
import godot.tools.common.constants.godotCorePackage
Expand All @@ -25,8 +26,9 @@ import java.io.File

private val cancellableContinuationClass = ClassName(kotlinxCoroutinePackage, "CancellableContinuation")
private val suspendCancellableCoroutine = MemberName(kotlinxCoroutinePackage, "suspendCancellableCoroutine")
private val connect = MemberName(godotExtensionPackage, "connectThreadSafe")
private val connectThreadSafe = MemberName(godotExtensionPackage, "connectThreadSafe")
private val resume = MemberName(kotlinCoroutinePackage, "resume")
private const val cancel = "cancel"

object AwaitGenerationService : IAwaitGenerationService {
override fun generate(output: File) {
Expand Down Expand Up @@ -124,14 +126,26 @@ object AwaitGenerationService : IAwaitGenerationService {
1 -> lambdaParameters
else -> "SignalArguments$argCount($lambdaParameters)"
}
val lambdaParametersWithType = buildString {
for (i in 0 until argCount) {
if (i != 0) {
append("")
}
append("p$i:·P$i")
}
}

return this
.beginControlFlow("return·%M", suspendCancellableCoroutine)
.addStatement("cont:·%T<%T>·->", cancellableContinuationClass, returnType)
.beginControlFlow("%M(%T.ConnectFlags.CONNECT_ONE_SHOT.id.toInt())", connect, GODOT_OBJECT)
.addStatement("$lambdaParameters·->")
.beginControlFlow("%M(", connectThreadSafe)
.addStatement("$lambdaParametersWithType·->")
.addStatement("cont.%M($resumeParameters)", resume)
.endControlFlow()
.beginControlFlow(".%M", AS_CALLABLE_UTIL_FUNCTION)
.addStatement("cont.%L()", cancel)
.endControlFlow()
.addCode(",·%T.ConnectFlags.CONNECT_ONE_SHOT.id.toInt())", GODOT_OBJECT)
.endControlFlow()
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,9 @@
package godot.codegen.services.impl

import com.squareup.kotlinpoet.ANY
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.LambdaTypeName
import com.squareup.kotlinpoet.MemberName
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.*
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.TypeVariableName
import godot.codegen.services.ILambdaCallableGenerationService
import com.squareup.kotlinpoet.UNIT
import godot.codegen.poet.GenericClassNameInfo
import godot.common.constants.Constraints
import godot.tools.common.constants.GodotFunctions
Expand All @@ -28,11 +17,12 @@ import java.io.File

object LambdaCallableGenerationService : ILambdaCallableGenerationService {
private const val FUNCTION_PARAMETER_NAME = "function"
private const val KT_CALLABLE_NAME = "LambdaCallable"
private const val LAMBDA_CALLABLE_NAME = "LambdaCallable"
private const val CALLABLE_FUNCTION_NAME = "callable"
private const val JAVA_CREATE_METHOD_NAME = "javaCreate"
private const val VARIANT_TYPE_ARGUMENT_NAME = "variantConverter"
private val KT_CALLABLE_CLASS_NAME = ClassName(godotCorePackage, KT_CALLABLE_NAME)
private const val ON_CANCEL_CALL_ARGUMENT_NAME = "onCancelCall"
private val LAMBDA_CALLABLE_CLASS_NAME = ClassName(godotCorePackage, LAMBDA_CALLABLE_NAME)
private val returnTypeParameter = TypeVariableName("R", ANY.copy(nullable = true))

//Java
Expand All @@ -42,21 +32,21 @@ object LambdaCallableGenerationService : ILambdaCallableGenerationService {
override fun generate(outputDir: File) {
val callableFileSpec = FileSpec.builder(godotCorePackage, "Callables")

val onDestroyCallLambdaType = LambdaTypeName.get(returnType = UNIT).copy(nullable = true)

for (argCount in 0..Constraints.MAX_FUNCTION_ARG_COUNT) {
val ktCallableClassName = ClassName(godotCorePackage, "$KT_CALLABLE_NAME$argCount")
val ktCallableClassName = ClassName(godotCorePackage, "$LAMBDA_CALLABLE_NAME$argCount")
val classBuilder = TypeSpec
.classBuilder(ktCallableClassName)
.superclass(
KT_CALLABLE_CLASS_NAME
LAMBDA_CALLABLE_CLASS_NAME
.parameterizedBy(returnTypeParameter)
)

val argumentRange = 0..<argCount

classBuilder
.addSuperclassConstructorParameter(
VARIANT_TYPE_ARGUMENT_NAME
)
classBuilder.addSuperclassConstructorParameter(VARIANT_TYPE_ARGUMENT_NAME)
classBuilder.addSuperclassConstructorParameter(ON_CANCEL_CALL_ARGUMENT_NAME)

for (index in argumentRange) {
classBuilder.addSuperclassConstructorParameter("p${index}Type")
Expand Down Expand Up @@ -139,6 +129,17 @@ object LambdaCallableGenerationService : ILambdaCallableGenerationService {
)
}

primaryConstructor
.addParameter(
ParameterSpec
.builder(
ON_CANCEL_CALL_ARGUMENT_NAME,
onDestroyCallLambdaType
)
.defaultValue("null")
.build()
)

primaryConstructor
.addParameter(
ParameterSpec
Expand Down Expand Up @@ -238,7 +239,7 @@ object LambdaCallableGenerationService : ILambdaCallableGenerationService {
var removedTypeVariables = 0
while (typeVariables.isNotEmpty()) {
val bindReturnType =
ClassName(godotCorePackage, "$KT_CALLABLE_NAME${typeVariableNames.size - typeVariables.size}")
ClassName(godotCorePackage, "$LAMBDA_CALLABLE_NAME${typeVariableNames.size - typeVariables.size}")
classBuilder.addFunction(
FunSpec.builder("bind")
.addParameters(
Expand All @@ -256,6 +257,8 @@ object LambdaCallableGenerationService : ILambdaCallableGenerationService {
append(",·p${index}Type")
}

append("$ON_CANCEL_CALL_ARGUMENT_NAME")

append(")·{·")

for (index in (0..<removedTypeVariables)) {
Expand Down Expand Up @@ -295,23 +298,31 @@ object LambdaCallableGenerationService : ILambdaCallableGenerationService {
.addTypeVariables(typeVariableNames.map { it.copy(reified = true) })
.addTypeVariable(returnTypeParameter.copy(reified = true))
.addModifiers(KModifier.INLINE)
.addParameter(
ParameterSpec
.builder(
FUNCTION_PARAMETER_NAME,
lambdaTypeName
)
.addModifiers(KModifier.NOINLINE)
.build()
.addParameters(
listOf(
ParameterSpec
.builder(ON_CANCEL_CALL_ARGUMENT_NAME, onDestroyCallLambdaType)
.addModifiers(KModifier.NOINLINE)
.defaultValue("null")
.build(),
ParameterSpec
.builder(
FUNCTION_PARAMETER_NAME,
lambdaTypeName
)
.addModifiers(KModifier.NOINLINE)
.build()
)
)
.addCode(
CodeBlock.of(
buildString {
append("return·$KT_CALLABLE_NAME$argCount(")
append("return·$LAMBDA_CALLABLE_NAME$argCount(")
append("%M.getOrDefault(%T::class,·%T),·")
for (typeParameter in typeVariableNames) {
append("%M[%T::class]!!,·")
}
append("$ON_CANCEL_CALL_ARGUMENT_NAME")
append(FUNCTION_PARAMETER_NAME)
append(')')
},
Expand All @@ -336,7 +347,14 @@ object LambdaCallableGenerationService : ILambdaCallableGenerationService {
.addTypeVariable(returnTypeParameter.copy(reified = true))
.addModifiers(KModifier.INLINE)
.receiver(lambdaTypeName)
.addCode("return·$CALLABLE_FUNCTION_NAME$argCount(this)")
.addParameter(
ParameterSpec
.builder(ON_CANCEL_CALL_ARGUMENT_NAME, onDestroyCallLambdaType)
.addModifiers(KModifier.NOINLINE)
.defaultValue("null")
.build()
)
.addCode("return·$CALLABLE_FUNCTION_NAME$argCount($ON_CANCEL_CALL_ARGUMENT_NAME,·this)")
.build()
)
}
Expand Down Expand Up @@ -382,11 +400,12 @@ object LambdaCallableGenerationService : ILambdaCallableGenerationService {
.addCode(
CodeBlock.of(
buildString {
append("return·$KT_CALLABLE_NAME$argCount(")
append("return·$LAMBDA_CALLABLE_NAME$argCount(")
append("%M.getOrDefault(%T.getOrCreateKotlinClass(returnClass),·%T),·")
genericClassNameInfo.toParameterSpecList().forEach {
append("%M[%T.getOrCreateKotlinClass(${it.name}Class)]!!,·")
}
append("null,·")
append(FUNCTION_PARAMETER_NAME)
append(')')
},
Expand Down
Loading

0 comments on commit ce8f70f

Please sign in to comment.