Skip to content
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

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion kotlin-native/runtime/src/launcher/cpp/launcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,14 @@ using namespace kotlin;
//--- Setup args --------------------------------------------------------------//

OBJ_GETTER(setupArgs, int argc, const char** argv) {
if (argc > 0 && argv[0][0] != '\0') {
Copy link
Member

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.

Copy link
Contributor Author

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.

// 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];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about doing kotlin::programName = strndup(argv[0], 4096)? No need to free the result, and we don't have to worry about lifetimes. 4096 is just some random not-too-small not-too-big number to protect us from argv[0] being too large. Technically, the OS has some limits already, but might as well protect ourselves.

Copy link
Contributor Author

@vonox7 vonox7 Jun 6, 2024

Choose a reason for hiding this comment

The 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;
Expand All @@ -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;
}

Expand Down
11 changes: 11 additions & 0 deletions kotlin-native/runtime/src/main/cpp/Runtime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ bool kotlin::initializeGlobalRuntimeIfNeeded() noexcept {
return true;
}

const char* kotlin::programName = nullptr;

extern "C" {

RUNTIME_NOTHROW void AppendToInitializersTail(InitNode *next) {
Expand Down Expand Up @@ -310,6 +312,15 @@ KBoolean Konan_Platform_isDebugBinary() {
return kotlin::compiler::shouldContainDebugInfo();
}

OBJ_GETTER0(Konan_Platform_getProgramName) {
if (kotlin::programName == nullptr) {
// null in case Platform.getProgramName is called from within a library and the main function of the binary is not built with Kotlin
RETURN_OBJ(nullptr)
} else {
RETURN_RESULT_OF(CreateStringFromCString, kotlin::programName)
}
}

bool Kotlin_memoryLeakCheckerEnabled() {
return g_checkLeaks;
}
Expand Down
2 changes: 2 additions & 0 deletions kotlin-native/runtime/src/main/cpp/Runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ namespace kotlin {
// Returns `true` if initialized.
bool initializeGlobalRuntimeIfNeeded() noexcept;

extern const char* programName;

}

#endif // RUNTIME_RUNTIME_H
11 changes: 11 additions & 0 deletions kotlin-native/runtime/src/main/kotlin/kotlin/native/Platform.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@qurbonzoda could you please review the stdlib change?

Copy link
Contributor

Choose a reason for hiding this comment

The 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
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11454,6 +11454,8 @@ final object kotlin.native/Platform { // kotlin.native/Platform|null[0]
final fun <get-memoryModel>(): kotlin.native/MemoryModel // kotlin.native/Platform.memoryModel.<get-memoryModel>|<get-memoryModel>(){}[0]
final val osFamily // kotlin.native/Platform.osFamily|{}osFamily[0]
final fun <get-osFamily>(): kotlin.native/OsFamily // kotlin.native/Platform.osFamily.<get-osFamily>|<get-osFamily>(){}[0]
final val programName // kotlin.native/Platform.programName|{}programName[0]
final fun <get-programName>(): kotlin/String? // kotlin.native/Platform.programName.<get-programName>|<get-programName>(){}[0]

final var isCleanersLeakCheckerActive // kotlin.native/Platform.isCleanersLeakCheckerActive|{}isCleanersLeakCheckerActive[0]
final fun <get-isCleanersLeakCheckerActive>(): kotlin/Boolean // kotlin.native/Platform.isCleanersLeakCheckerActive.<get-isCleanersLeakCheckerActive>|<get-isCleanersLeakCheckerActive>(){}[0]
Expand Down
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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add an explicit return 0;?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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")
}
11 changes: 11 additions & 0 deletions native/native.tests/testData/programName/kotlinPrintEntryPoint.kt
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()}")
}
15 changes: 15 additions & 0 deletions native/native.tests/testData/programName/main.c
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]));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw, execv requires that the list of arguments must be terminated by a NULL pointer. Is it always guaranteed here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

argv is an array of size argc + 1 and the last member is always NULL: https://en.cppreference.com/w/c/language/main_function


printf("exec failed with errno=%d\n", errno);
return 1;
}
11 changes: 11 additions & 0 deletions native/native.tests/testData/standalone/entryPoint/programName.kt
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:")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Linux, this fails for me with

java.lang.AssertionError: Expected <calling exec...
programName: null
args:>, actual <calling exec...
programName: 
args:>.

Please take a look.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 programName=null, and empty programName leads to programName="".
B) all 3 platforms behave the same; however both no program name and empty program Name lead to programName="". (the linux way of thinking)
C) Fully native behaviour: no programName will be programName="" on linux and programName=null on macOS/windows; and empty programName will be everywhere programName="".


/*
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")
}
}