-
Notifications
You must be signed in to change notification settings - Fork 5.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[K/N] Expose program name in runtime #5281
Changes from all commits
5667613
8b7046f
c22754d
5879172
788de0f
855917c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,8 +30,14 @@ using namespace kotlin; | |
//--- Setup args --------------------------------------------------------------// | ||
|
||
OBJ_GETTER(setupArgs, int argc, const char** argv) { | ||
if (argc > 0 && argv[0][0] != '\0') { | ||
// Don't set the programName to an empty string (by checking argv[0][0] != '\0') to make all platforms behave the same: | ||
// Linux would set argv[0] to "" in case no programName is passed, whereas Windows & macOS would set argc to 0. | ||
kotlin::programName = argv[0]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about doing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, I applied your idea. |
||
} | ||
|
||
// The count is one less, because we skip argv[0] which is the binary name. | ||
ObjHeader* result = AllocArrayInstance(theArrayTypeInfo, argc - 1, OBJ_RESULT); | ||
ObjHeader* result = AllocArrayInstance(theArrayTypeInfo, std::max(0, argc - 1), OBJ_RESULT); | ||
ArrayHeader* array = result->array(); | ||
for (int index = 1; index < argc; index++) { | ||
ObjHolder result; | ||
|
@@ -58,6 +64,8 @@ extern "C" RUNTIME_EXPORT int Init_and_run_start(int argc, const char** argv, in | |
Kotlin_shutdownRuntime(); | ||
} | ||
|
||
kotlin::programName = nullptr; // argv[0] might not be valid after this point | ||
|
||
return exitStatus; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -102,6 +102,13 @@ public object Platform { | |
public val isFreezingEnabled: Boolean | ||
get() = false | ||
|
||
/** | ||
* Representation of the name used to invoke the program executable. | ||
* [null] if the Kotlin code was compiled to a native library and the executable is not a Kotlin program. | ||
*/ | ||
public val programName: String? | ||
get() = Platform_getProgramName() | ||
SvyatoslavScherbina marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @qurbonzoda could you please review the stdlib change? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LGTM |
||
|
||
/** | ||
* If the memory leak checker is activated, by default `true` in debug mode, `false` in release. | ||
* When memory leak checker is activated, and leak is detected during last Kotlin context | ||
|
@@ -156,6 +163,10 @@ private external fun Platform_getCpuArchitecture(): Int | |
@GCUnsafeCall("Konan_Platform_isDebugBinary") | ||
private external fun Platform_isDebugBinary(): Boolean | ||
|
||
@GCUnsafeCall("Konan_Platform_getProgramName") | ||
@Escapes.Nothing | ||
private external fun Platform_getProgramName(): String? | ||
|
||
@GCUnsafeCall("Konan_Platform_getMemoryLeakChecker") | ||
private external fun Platform_getMemoryLeakChecker(): Boolean | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
#include <stdio.h> | ||
#include "programName_api.h" | ||
|
||
int main() { | ||
programName(); | ||
fflush(NULL); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's add an explicit There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok, done |
||
return 0; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Platform.programName is null within library |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
@file:OptIn(kotlin.experimental.ExperimentalNativeApi::class) | ||
|
||
@CName("programName") | ||
fun programName() { | ||
println("Platform.programName is " + Platform.programName + " within library") | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
@file:OptIn(kotlin.experimental.ExperimentalNativeApi::class) | ||
|
||
import kotlin.native.Platform | ||
|
||
fun main(args: Array<String>) { | ||
// Remove path (using both unix/win path seperators) and extension (.kexe or .exe) | ||
val programFileName = Platform.programName?.substringAfterLast("/")?.substringAfterLast("\\")?.substringBeforeLast(".") | ||
|
||
println("programName: $programFileName") | ||
println("args: ${args.joinToString()}") | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
#include <errno.h> | ||
#include <stdio.h> | ||
#include <unistd.h> | ||
|
||
int main(int argc, char *argv[]) { | ||
printf("calling exec...\n"); | ||
fflush(NULL); | ||
|
||
// Kotlin executable name is in argv[1] | ||
// Forward argv[2..n] to kotlin executable as arguments (the first one should be the programName according to posix) | ||
execv(argv[1], &(argv[2])); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Btw, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
printf("exec failed with errno=%d\n", errno); | ||
return 1; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
@file:OptIn(kotlin.experimental.ExperimentalNativeApi::class) | ||
|
||
import kotlin.native.Platform | ||
import kotlin.test.* | ||
|
||
fun main(args: Array<String>) { | ||
// Remove path and extension (.kexe or .exe) | ||
val programFileName = Platform.programName!!.substringAfterLast("/").substringAfterLast("\\").substringBeforeLast(".") | ||
|
||
assertEquals("standalone_entryPoint_programName", programFileName) | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
/* | ||
* Copyright 2010-2024 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.konan.target.Family | ||
import org.jetbrains.kotlin.konan.test.blackbox.support.TestCase | ||
import org.jetbrains.kotlin.konan.test.blackbox.support.TestKind | ||
import org.jetbrains.kotlin.konan.test.blackbox.support.compilation.TestCompilationResult.Companion.assertSuccess | ||
import org.jetbrains.kotlin.konan.test.blackbox.support.settings.testProcessExecutor | ||
import org.jetbrains.kotlin.konan.test.blackbox.support.util.ClangDistribution | ||
import org.jetbrains.kotlin.konan.test.blackbox.support.util.compileWithClang | ||
import org.jetbrains.kotlin.native.executors.runProcess | ||
import org.junit.jupiter.api.Tag | ||
import org.junit.jupiter.api.Test | ||
import java.io.File | ||
import kotlin.test.assertEquals | ||
import kotlin.time.Duration.Companion.seconds | ||
|
||
@Tag("program-name") | ||
class ProgramNameTest : AbstractNativeSimpleTest() { | ||
|
||
@Test | ||
fun programNameTest() { | ||
// 1. Compile kotlinPrintEntryPoint.kt to kotlinPrintEntryPoint.kexe | ||
|
||
val kotlinCompilation = compileToExecutableInOneStage( | ||
generateTestCaseWithSingleFile( | ||
sourceFile = sourceDir.resolve("kotlinPrintEntryPoint.kt"), | ||
testKind = TestKind.STANDALONE_NO_TR, | ||
extras = TestCase.NoTestRunnerExtras("main") | ||
) | ||
).assertSuccess() | ||
|
||
// 2. Compile main.c to main.cexe | ||
|
||
val cExecutable = buildDir.resolve("main.cexe") | ||
compileWithClang( | ||
clangDistribution = ClangDistribution.Llvm, | ||
sourceFiles = listOf(sourceDir.resolve("main.c")), | ||
outputFile = cExecutable, | ||
additionalClangFlags = listOf("-Wall", "-Werror"), | ||
).assertSuccess() | ||
|
||
// 3. run main.cexe (with different parameters) to call kotlin executable | ||
|
||
fun validate(expected: String, vararg args: String) { | ||
val binaryName = kotlinCompilation.resultingArtifact.executableFile.path | ||
val result = testRunSettings.testProcessExecutor.runProcess(cExecutable.absolutePath, binaryName, *args) { | ||
timeout = 60.seconds | ||
} | ||
val sanitizedStdout = result.stdout.replace("\r\n", "\n") // Ignore if we have unix or windows line endings | ||
assertEquals("calling exec...\n$expected", sanitizedStdout) | ||
assertEquals("", result.stderr) | ||
} | ||
|
||
// kotlinPrintEntryPoint removes .kexe | ||
validate("programName: app\nargs:", "app.kexe") | ||
|
||
// Simulate a custom program name, see e.g. https://busybox.net/downloads/BusyBox.html#usage | ||
validate("programName: customProgramName\nargs:", "customProgramName") | ||
validate("programName: customProgramName\nargs: firstArg, secondArg", "customProgramName", "firstArg", "secondArg") | ||
|
||
if (targets.testTarget.family == Family.MINGW) { | ||
// With MinGW, `execv` in `main.c` fails with errno=22 (EINVAL) in the tests below. | ||
// Let's just skip those corner cases. | ||
return | ||
} | ||
|
||
// No program name - this would not be POSIX compliant, see https://pubs.opengroup.org/onlinepubs/9699919799/functions/exec.html: | ||
// "[...] requires a Strictly Conforming POSIX Application to pass at least one argument to the exec function" | ||
// However, we should not crash the Kotlin runtime because of this. | ||
validate("programName: null\nargs:") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On Linux, this fails for me with
Please take a look. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is very interesting. Linux behaves here differently as windows & macOS. I fixed the problem, and added more explanation in the code. I used the following C standard PDF: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf. Please tell me if you would want to let windows+macOS behave like linux. The way I wrote it now feels more natural to me, however I fully understand that all 3 options have its benefits and drawbacks: A) The way I wrote it now: all 3 platforms behave the same; no program name leads to |
||
|
||
/* | ||
An empty programName is treated as "no program name", because of the following reasoning: | ||
|
||
When providing no program name (see above validation), both macOS and windows result in an empty argv (argv=[]). | ||
However, linux behaves differently and sets argv=[""]. | ||
|
||
The C standard in section "5.1.2.2.1 Program startup" states: | ||
"argv[0][0] shall be the null character if the program name is not available from the host environment". | ||
|
||
It is discussable if en empty program name is considered as "not available from the host environment", | ||
however in the sake of consistency across platforms we decided to treat it as such. | ||
|
||
Therefore, the following test with an empty program name is expected to result in programName=null. | ||
*/ | ||
|
||
validate("programName: null\nargs:", "") | ||
} | ||
|
||
companion object { | ||
private val sourceDir = File("native/native.tests/testData/programName") | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need a comment explaining the purpose of
argv[0][0] != '\0'
check.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, I also commented it here and not only indirectly in the testcase.