diff --git a/kotlin-native/llvmDebugInfoC/src/scripts/konan_lldb.py b/kotlin-native/llvmDebugInfoC/src/scripts/konan_lldb.py index ee32be370e6..3db64c28831 100644 --- a/kotlin-native/llvmDebugInfoC/src/scripts/konan_lldb.py +++ b/kotlin-native/llvmDebugInfoC/src/scripts/konan_lldb.py @@ -627,6 +627,39 @@ class KonanStepOut(KonanStep): return self.thread_plan.QueueThreadPlanForStepOut(0) +KONAN_LLDB_DONT_SKIP_BRIDGING_FUNCTIONS = 'KONAN_LLDB_DONT_SKIP_BRIDGING_FUNCTIONS' +MAX_SIZE_FOR_STOP_REASON = 20 +PLAN_FROM_STOP_REASON = { + 'step in' : KonanStepIn.__name__, + 'step out' : KonanStepOut.__name__, + 'step over' : KonanStepOver.__name__, +} + +class KonanHook: + def __init__(self, target, extra_args, _): + pass + + def handle_stop(self, execution_context, stream) -> bool: + is_bridging_functions_skip_enabled = not execution_context.target.GetEnvironment().Get(KONAN_LLDB_DONT_SKIP_BRIDGING_FUNCTIONS) + + def is_kotlin_bridging_function() -> bool: + addr = execution_context.frame.addr + function_name = addr.function.name + if function_name is None: + return False + file_name = addr.line_entry.file.basename + if file_name is None: + return False + return function_name.startswith('objc2kotlin_') and file_name == '' + + if is_bridging_functions_skip_enabled and is_kotlin_bridging_function(): + stop_reason = execution_context.frame.thread.GetStopDescription(MAX_SIZE_FOR_STOP_REASON) + plan = PLAN_FROM_STOP_REASON.get(stop_reason) + if plan is not None: + execution_context.thread.StepUsingScriptedThreadPlan('{}.{}'.format(__name__, plan)) + return True + + def __lldb_init_module(debugger, _): log(lambda: "init start") __FACTORY['object'] = lambda x, y, z: KonanObjectSyntheticProvider(x, y, z) @@ -652,4 +685,5 @@ def __lldb_init_module(debugger, _): debugger.HandleCommand('command script add -f {}.symbol_by_name_command symbol_by_name'.format(__name__)) # Avoid Kotlin/Native runtime debugger.HandleCommand('settings set target.process.thread.step-avoid-regexp ^::Kotlin_') + debugger.HandleCommand('target stop-hook add -P {}.KonanHook'.format(__name__)) log(lambda: "init end") diff --git a/native/native.tests/tests/org/jetbrains/kotlin/konan/test/blackbox/ObjCToKotlinSteppingInLLDBTest.kt b/native/native.tests/tests/org/jetbrains/kotlin/konan/test/blackbox/ObjCToKotlinSteppingInLLDBTest.kt new file mode 100644 index 00000000000..f6521d4bd4a --- /dev/null +++ b/native/native.tests/tests/org/jetbrains/kotlin/konan/test/blackbox/ObjCToKotlinSteppingInLLDBTest.kt @@ -0,0 +1,276 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.konan.test.blackbox + +import org.jetbrains.kotlin.incremental.createDirectory +import org.jetbrains.kotlin.konan.test.blackbox.support.* +import org.jetbrains.kotlin.konan.test.blackbox.support.compilation.ObjCFrameworkCompilation +import org.jetbrains.kotlin.konan.test.blackbox.support.compilation.TestCompilationArtifact +import org.jetbrains.kotlin.konan.test.blackbox.support.compilation.TestCompilationResult.Companion.assertSuccess +import org.jetbrains.kotlin.konan.test.blackbox.support.runner.TestExecutable +import org.jetbrains.kotlin.konan.test.blackbox.support.runner.TestRunCheck +import org.jetbrains.kotlin.konan.test.blackbox.support.runner.TestRunCheck.ExecutionTimeout +import org.jetbrains.kotlin.konan.test.blackbox.support.runner.TestRunChecks +import org.jetbrains.kotlin.konan.test.blackbox.support.settings.LLDB +import org.jetbrains.kotlin.konan.test.blackbox.support.settings.PipelineType +import org.jetbrains.kotlin.konan.test.blackbox.support.settings.Timeouts +import org.jetbrains.kotlin.konan.test.blackbox.support.settings.configurables +import org.jetbrains.kotlin.konan.test.blackbox.support.util.LLDBSessionSpec +import org.junit.jupiter.api.Assumptions +import org.junit.jupiter.api.Test +import java.io.File + + +@EnforcedProperty(ClassLevelProperty.COMPILER_OUTPUT_INTERCEPTOR, "NONE") +@EnforcedHostTarget +class ObjCToKotlinSteppingInLLDBTest : AbstractNativeSimpleTest() { + + @Test + fun stepInFromObjCToKotlin___WithDisabledStopHook___StopsAtABridgingRoutine() { + testSteppingFromObjcToKotlin( + """ + > b ${CLANG_FILE_NAME}:3 + > env KONAN_LLDB_DONT_SKIP_BRIDGING_FUNCTIONS=1 + > run + > thread step-in + [..] stop reason = step in + [..]`objc2kotlin_kfun:#bar(){} at :1 + > c + """.trimIndent(), + CLANG_FILE_NAME, + KOTLIN_FILE_NAME, + "${ObjCToKotlinSteppingInLLDBTest::class.qualifiedName}.${::stepInFromObjCToKotlin___WithDisabledStopHook___StopsAtABridgingRoutine.name}" + ) + } + + @Test + fun stepInFromObjCToKotlin___WithStopHook___StepsThroughToKotlinCode() { + testSteppingFromObjcToKotlin( + """ + > b ${CLANG_FILE_NAME}:3 + > run + > thread step-in + [..] stop reason = Python thread plan implemented by class konan_lldb.KonanStepIn. + [..]`kfun:#bar(){} at lib.kt:1:1 + -> 1 fun bar() { + ^ + 2 print("") + 3 } + > c + """.trimIndent(), + CLANG_FILE_NAME, + KOTLIN_FILE_NAME, + "${ObjCToKotlinSteppingInLLDBTest::class.qualifiedName}.${::stepInFromObjCToKotlin___WithStopHook___StepsThroughToKotlinCode.name}" + ) + } + + @Test + fun stepOutFromKotlinToObjC___WithDisabledStopHook___StopsAtABridgingRoutine() { + testSteppingFromObjcToKotlin( + """ + > b ${KOTLIN_FILE_NAME}:2 + > env KONAN_LLDB_DONT_SKIP_BRIDGING_FUNCTIONS=1 + > run + > thread step-out + [..] stop reason = step out + [..]`objc2kotlin_kfun:#bar(){} at :1 + > c + """.trimIndent(), + CLANG_FILE_NAME, + KOTLIN_FILE_NAME, + "${ObjCToKotlinSteppingInLLDBTest::class.qualifiedName}.${::stepOutFromKotlinToObjC___WithDisabledStopHook___StopsAtABridgingRoutine.name}" + ) + } + + @Test + fun stepOutFromKotlinToObjC___WithStopHook___StepsOutToObjCCode() { + testSteppingFromObjcToKotlin( + """ + > b ${KOTLIN_FILE_NAME}:2 + > run + > thread step-out + [..] stop reason = Python thread plan implemented by class konan_lldb.KonanStepOut. + [..]`main at main.m:3:5 + 1 @import Kotlin; + 2 int main() { + -> 3 [KotlinLibKt bar]; + ^ + 4 } + > c + """.trimIndent(), + CLANG_FILE_NAME, + KOTLIN_FILE_NAME, + "${ObjCToKotlinSteppingInLLDBTest::class.qualifiedName}.${::stepOutFromKotlinToObjC___WithStopHook___StepsOutToObjCCode.name}" + ) + } + + @Test + fun stepOverFromKotlinToObjC___WithDisabledStopHook___StopsAtABridgingRoutine() { + testSteppingFromObjcToKotlin( + """ + > b ${KOTLIN_FILE_NAME}:3 + > env KONAN_LLDB_DONT_SKIP_BRIDGING_FUNCTIONS=1 + > run + > thread step-over + [..] stop reason = step over + [..]`objc2kotlin_kfun:#bar(){} at :1 + > c + """.trimIndent(), + CLANG_FILE_NAME, + KOTLIN_FILE_NAME, + "${ObjCToKotlinSteppingInLLDBTest::class.qualifiedName}.${::stepOverFromKotlinToObjC___WithDisabledStopHook___StopsAtABridgingRoutine.name}" + ) + } + + @Test + fun stepOverFromKotlinToObjC___WithStopHook___StepsOverToObjCCode() { + testSteppingFromObjcToKotlin( + """ + > b ${KOTLIN_FILE_NAME}:3 + > run + > thread step-over + [..] stop reason = Python thread plan implemented by class konan_lldb.KonanStepOver. + [..]`main at main.m:3:5 + 1 @import Kotlin; + 2 int main() { + -> 3 [KotlinLibKt bar]; + ^ + 4 } + > c + """.trimIndent(), + CLANG_FILE_NAME, + KOTLIN_FILE_NAME, + "${ObjCToKotlinSteppingInLLDBTest::class.qualifiedName}.${::stepOverFromKotlinToObjC___WithStopHook___StepsOverToObjCCode.name}" + ) + } + + private fun testSteppingFromObjcToKotlin( + lldbSpec: String, + clangFileName: String, + kotlinFileName: String, + testName: String, + ) { + if (!targets.testTarget.family.isAppleFamily) { Assumptions.abort("This test is supported only on Apple targets") } + + val kotlinFrameworkName = "Kotlin" + val clangMainSources = """ + @import ${kotlinFrameworkName}; + int main() { + [${kotlinFrameworkName}LibKt bar]; + } + """.trimIndent() + + val kotlinLibrarySources = """ + fun bar() { + print("") + } + """.trimIndent() + + runTestWithLLDB( + kotlinLibrarySources = kotlinLibrarySources, + kotlinFileName = kotlinFileName, + kotlinFrameworkName = kotlinFrameworkName, + clangMainSources = clangMainSources, + clangFileName = clangFileName, + lldbSpec = lldbSpec, + testName = testName, + ) + } + + private fun runTestWithLLDB( + kotlinLibrarySources: String, + kotlinFileName: String, + kotlinFrameworkName: String, + clangMainSources: String, + clangFileName: String, + lldbSpec: String, + testName: String, + ) { + // 1. Create sources + val sourceDirectory = buildDir.resolve("sources") + sourceDirectory.createDirectory() + val clangFile = sourceDirectory.resolve(clangFileName) + clangFile.writeText(clangMainSources) + sourceDirectory.resolve(kotlinFileName).writeText(kotlinLibrarySources) + + // 2. Build Kotlin framework + val freeCompilerArgs = TestCompilerArgs( + testRunSettings.get().compilerFlags + listOf( + "-Xstatic-framework", + "-Xbinary=bundleId=stub", + ) + ) + val module = generateTestCaseWithSingleModule(sourceDirectory, freeCompilerArgs) + ObjCFrameworkCompilation( + testRunSettings, + freeCompilerArgs = freeCompilerArgs, + sourceModules = module.modules, + dependencies = emptyList(), + expectedArtifact = TestCompilationArtifact.ObjCFramework( + buildDir, + kotlinFrameworkName, + ) + ).result.assertSuccess() + + // 3. Compile the executable + val clangExecutableName = "clangMain" + val executableFile = File(buildDir, clangExecutableName) + + // FIXME: absoluteTargetToolchain might not work correctly with KONAN_USE_INTERNAL_SERVER because + // :kotlin-native:dependencies:update is not a dependency of :native:native.tests:test where this test runs + val process = ProcessBuilder( + "${testRunSettings.configurables.absoluteTargetToolchain}/bin/clang", + clangFile.absolutePath, + "-isysroot", testRunSettings.configurables.absoluteTargetSysRoot, + "-target", testRunSettings.configurables.targetTriple.toString(), + "-g", "-fmodules", + "-F", buildDir.absolutePath, + "-o", executableFile.absolutePath + ).redirectErrorStream(true).start() + val clangOutput = process.inputStream.readBytes() + + check( + process.waitFor() == 0 + ) { clangOutput.decodeToString() } + + // 4. Generate the test case + val testExecutable = TestExecutable( + TestCompilationArtifact.Executable(executableFile), + loggedCompilationToolCall = LoggedData.NoopCompilerCall(buildDir), + testNames = listOf(TestName(testName)), + ) + val spec = LLDBSessionSpec.parse(lldbSpec) + val moduleForTestCase = TestModule.Exclusive(testName, emptySet(), emptySet(), emptySet()) + val testCase = TestCase( + id = TestCaseId.Named(testName), + kind = TestKind.STANDALONE_LLDB, + modules = setOf(moduleForTestCase), + freeCompilerArgs = freeCompilerArgs, + nominalPackageName = PackageName.EMPTY, + checks = TestRunChecks( + executionTimeoutCheck = ExecutionTimeout.ShouldNotExceed(testRunSettings.get().executionTimeout), + exitCodeCheck = TestRunCheck.ExitCode.Expected(0), + expectedFailureCheck = null, + outputDataFile = null, + outputMatcher = spec.let { TestRunCheck.OutputMatcher { output -> spec.checkLLDBOutput(output, testRunSettings.get()) } }, + fileCheckMatcher = null, + ), + extras = TestCase.NoTestRunnerExtras( + "main", + arguments = spec.generateCLIArguments(testRunSettings.get().prettyPrinters) + ) + ).apply { initialize(null, null) } + + // 5. Run the test + runExecutableAndVerify(testCase, testExecutable) + } + + companion object { + val CLANG_FILE_NAME = "main.m" + val KOTLIN_FILE_NAME = "lib.kt" + } + +}