SE-0513: API to get the path to the current executable

FWIW, CommandLine already has unsafeArgv whose type is UnsafeMutablePointer<UnsafeMutablePointer<Int8>?>, so that use case feels like it's already reasonably covered. Offering the fully unsafe version, the fully safe [String] version, and a middle-ground array of arrays of CChar version would be quite a bit of API surface.

So the question is whether that middle use case is worth optimizing for. Maybe? It's obviously true that many elements of argv are going to be file paths, so if you need to support paths that aren't valid UTF-8, you would have to use unsafeArgv instead of arguments. I can almost guarantee that most Swift code in production that deals with command lines isn't handling this correctly today.

This is one of those times where I wish we had a better "bag of bytes" or "platform string" type besides [UInt8] or [CChar] because the type system could express exactly what is intended here and it could provide good APIs to convert them to more specific representations. Obviously outside the scope of this proposal, of course.

1 Like

I'm aware of unsafeArgv, but note just how unsafe it is: it's global mutable state! We don't like that! So yes, I'd be advocating for (beyond this proposal) revisiting unsafeArgv and providing something that lets us deprecate and replace unsafeArgv outright with the same basic semantics as executablePathCString.

Long ago, we had CString, but then the Fire Nation attacked Swift as a whole went in a different direction and said "C strings are pointers and Swift strings are Unicode".

This property is explicitly unavailable in Embedded Swift and WASI: if in the future we can reliably get a value for this property in Embedded Swift or on WASI, we ought to be able to lift these constraints.

It makes sense to make it unavailable for Embedded Swift, but I don't think so for WASI. I think just returning argv[0] is better than making it unavailable.

A WASI executable is typically executed within a sandbox environment, so I believe argv[0] is sufficient for executablePath on WASI. If it's unavailable on WASI, we can't easily use executablePath when creating a cross-platform command-line tool that supports WASI. Thus, that also becomes a problem for other platforms.

1 Like

Okay: let's say we make it available on WASI. What are you doing with it where argv[0] (an arbitrary string of unknown nature) is an appropriate substitute? What is the code that needs executablePath but is entirely happy to take an arbitrary string on WASI?

It won't be an arbitrary string on WASI, it'll be a name of the module or the component containing the module. IMO it should return CommandLine.arguments.first on WASI and nil for non-WASI Embedded Swift.

In its current form the proposal making it unavailable on those platforms gets a -1 from me.

Packages that adopted CommandLine.arguments.first work on all those platforms, but if they adopt API from the proposal as currently stated they'll become less portable, which IMO is worse than the status quo.

1 Like

argv[0] is arbitrary per the C standard. If WASI provides stronger guarantees, are they documented anywhere?

Is __progname or __progname_full (from Musl) available in wasi-libc? Perhaps we could use one or the other?

Edit: Emscripten sets it. I don't see any references to either in WasmKit or wasmtime.

You shouldn't rely on presence of WASI-libc when building Swift for arbitrary WASI version. I could see a package adopting WASI 0.2/0.3+ syscalls directly without WASI-libc in the future, we would call WASI CLI syscalls directly in CommandLine.arguments. In this scenario CommandLine.arguments.first would be the right implementation for both embedded and non-embedded packages supporting WASI. For the record, I don't have an opinion preference for how other platforms implement it.

Well, it was just a thought. :slightly_smiling_face:

I'd refer you to the motivation section of this proposal; if argv[0] (or CommandLine.arguments.first—same thing) were appropriate for this purpose in the general case, this API wouldn't be necessary in the first place.

If Wasm or WASI gives stricter guarantees about the value of argv[0] and we can refer back to documentation on the subject, I'd be happy to revise the proposal to use argv[0] there, but I'm not aware of any such guarantees. Can you point me to any relevant documentation?

1 Like

Biiiiig +1 from me as well. Currently, we have SwiftFoundation (re)implementing this exact logic in Platform.getFullExecutablePath; Bundle also heavily depends on this functionality to work.

1 Like

How does this proposal plan to implement the feature? Presumably, it has to receive this information from the operating system somehow, so as an alternative, can't you call out to these APIs directly like Boost does?

CommandLine is just a common interface, specified in C, for calling a function in an executable file, passing in a list of strings and receiving back a number. It doesn't make any other assumptions, not even that there's a filesystem.

This should be placed together with a cross-platform package that implements the other POSIX-style assumptions, like environment variables and a filesystem; not CommandLine.


I think as another alternative, the arguments, environment variables, and other context should be passed to the init or main function, depending on what's available from the environment, but without assuming that these exist on all platforms (e.g. WASM).

Currently, the "CommandLine" interface is an empty enum that contains static properties whose value changes from process to process. You can't actually create an instance of CommandLine and fill it with values.

Instead, I should be able to define a main function that can receive values such as the command line arguments, environment variables, and current executable file path, whatever is available from the calling environment. Then all other functions/methods are identical between environments, and cannot see environment differences except for what is read and handled by the main/init function.

The proposal has a link to the implementation, which you can look at.

Proposal reviews should generally be less concerned about "how" something is implemented than "that" it can be implemented; however, the line can perhaps be blurred in situations like this where an API exposes platform details that are inherently implementation-specific. If you have specific concerns about how implementation choices might impact the design of the API, or what user-facing behavior would be surfaced by those choices, of course those are fair game.

Ah, I did pass over this link entirely. Yeah, I wouldn't expect this proposal to make system calls; this is only weakly hinted at even though it's presented as a feature of the programming language, in order to suspect this, I would at least first have to know that this isn't a feature of the actual interface of main that the parent calls.

If CommandLine is going to be the implementation of the common, environment-independent standard that works across platforms and the programming languages they're calling into, then resolving the executable path should be in a separate operating system API or system call package (though a cross platform one).

Are there intended use cases for this feature besides the two examples from the first thread? I see the initial pitch includes two examples, but neither are interoperable:

posix_spawn(executablePath, ...) Usually you want to use fork instead, which will spawn the child even if the executable is moved or unlinked.

let resourcesDirectory = "\(executablePath)-resources" This assumes that the resources will always be in the same location as the executable, but most -nix distributions do not accommodate this. You're still reliant on environment-specific patterns to distribute and locate resources, for example, Bundle is exactly the pattern you are supposed to use if you need to bundle resources with the executable, because a raw executable is insufficient. (i.e. The proposal's observation that Bundle is 'fairly "high up" in the stack' isn't a bug, it's the solution.)

fork() creates a new process with a clone of the parent's state including its address space, file descriptors, etc. But it doesn't copy any of the other threads of a multithreaded program; the call to fork() acts much like a call to a signal handler, and in fact it is unsafe to do anything in the child process that isn't explicitly async-signal-safe.

You cannot safely call fork() from Swift[1]. See for example this past discussion about it. Oh, and this short paper from Microsoft Research.

On Darwin in particular, fork() is effectively deprecated and is only ever really seen in code that's a straight port from some flavour of UNIX. If a forked child process touches Core Foundation before calling execve() (or related), the process terminates. In the general case, a program written in Swift and running on Darwin is probably linking against Core Foundation directly or indirectly.

While I wouldn't advise the average Swift developer to go reimplementing Bundle, Bundle itself needs to look up the executable path of the current process in order to implement resource discovery. :slight_smile:


  1. You can call fork() from C/C++ in a program that also includes Swift, but only if you call execve() (or related) immediately afterward. This is effectively how posix_spawn() is implemented on some platforms, of course. ↩︎

2 Likes

Could you please elaborate on why you believe that? fork is definitely far riskier than posix_spawn and possibly can even leak resources unless you are extremely careful. The general recommendation tends to be to prfer posix_spawn.

While here ...

  • What is your evaluation of the proposal?

I think that this is a worthy addition to the current API surface.

  • Is the problem being addressed significant enough to warrant a change to Swift?

I feel like this problem comes up every so often, so, yes, its something that does warrant a change to Swift.

  • Does this proposal fit well with the feel and direction of Swift?

Yes

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

I think that it is decently comparable to the Boost solution. I think that it might possibly be better as: public static var executablePath: String { get throws }, but the current shape is reasonable as well. A throwing variant might allow for more error information to propagate.

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

A quick glance through the proposal and a peek into the implementation.

2 Likes

How does this fit with the pitch of adding a FilePath type to the standard library?

That's an open question right now. If we end up adding such a type, it makes sense for this property to have that type. However that's still just a pitch, and one for which there is a fair bit of active discussion, so I don't think we can guess what the outcome will be.

We may end up delaying this proposal until that question is resolved. That's for the LSG to decide.

5 Likes

So? fork() is the given POSIX API for doing this in an interoperable manner. If Swift has problems with forking the process, let's file a new proposal.

Who's proposing developers re-implement Bundle?

I'm not saying "looking up the executable path" is always bad, I'm saying building a bundle then calling Bundle.resourcePath is the correct framework to do this in a repeatable manner.

This isn't a meaningful distinction to my point... posix_spawn is just a special case of a fork then exec. The point is you don't need a file on the filesystem, you don't even need a filesystem to spawn children with the correct API.

This discussion of fork vs. posix_spawn and how they are supported (or not) by Swift would be better done in a separate thread; it's fairly tangential to the API being proposed.

If you want to ask for more concrete motivating use cases that don't involve spawning new processes, that's fine, but that goal isn't served by doing a more technical deep dive of fork here.

2 Likes

This proposal has been returned for revision. Thanks to everyone who participated in the review!