`RemoteCallTarget` from stable names

Hello Swift Forums,

Dating back to 2022 when distributed actors are officially introduced in Swift, we made the decision of using an opaque String value as the identifier of a remote call. In practice the string holds the mangled name of the distributed func thunk from which type information can be extracted and function entry can be located.

The current mechanism works pretty well if we’re using Swift for both the client and the server, and we decide to encode and decode the string within the remote call envelope. Things are becoming tricky if we want to avoid that (for the security sake or for interop with other languages / existing implementations). What we really want here is referenced to as stable names in the future directions of the distributed actor runtime proposal.

Here're some attempts in pursue of this direction under the current limitation. Erlang Actor system uses @StableName and @StableNames macros to generate two-way mappings between stable and mangled names as properties, and uses runtime check for HasStableNames protocol to opt-in the stable names. My own prototype, in a similar manner, utilizes the raw identifier feature coming in Swift 6.2 and a single @DistributedIdentifierMirror macro to embed a single-way "identifier to mangled name" mapping in customMirror for inspection from the callee's site, and use CwlDemangle to demangle and extract the raw identifier at the caller's site.

Both of the two do not address the problem of versioning, where a mangled name might change even without breaking API compatibility (I guess the latest @abi attribute can help here). The unresolved, major problem here is that mangling in Swift is hard, and only the compiler is capable of doing it right. By using a combination of syntatic generation by macro and custom mangler implementation, we're risking being out of sync with the compiler and generating an incorrect mangled name (eg. when ABI-affecting features like raw identifiers or sending are introduced). It's also not possible to pre-generate these names as static strings, because mangling happens after SIL while macros are applied during AST generation.

Alternative... maybe?

If we have a way to get all mangled names from the runtime reflection metadata, things could be easier. I remembered Azoy did something similar for subcommand auto discovery in the argument parser, but that’s a lot of C boilerplate and largely overkill. It also has negative impact on performance because a scan will happen at runtime, while distributed actor magic should be done at the compile time if possible.

Thinking about how executeDistributedTarget is implemented: it first decodes and demangles the identifier in the RemoteCallTarget, gets the function type from it with generic substitutions, then locates and executes the underlying function. If we are able to utilize a hypothetical DistributedFunctionKeyPath to reimplement RemoteCallTarget, we're able to ensure a safe manner for both referencing the function by mangled string (because existence can be checked during initialization) and stable name (which can be mapped to a key path losslessly). We may even gain performance boost from not demangling the identifier repeatedly.

To conclude, stable names are a vital key for enabling interoperation between Swift distributed actors and existing protocols or actors from other languages. It's also important for a stable behavior that ensures forward and backward compatibility. It's easy to express the stable names by macro or raw identifiers, but we need some improvements on RemoteCallTarget to make it work seamlessly, at least without the need of hacking on mangling. I'm posting this as a personal summary of how this feature can be implemented and feel free to discuss what we can do next to make it into reality.


This post is a rewritten version of the original one on Swift Open Source Slack, which I will attach below, with the knowledge of Erlang Actor system.

Thanks sincerely to @ktoso for mentioning the repo and suggesting this forum post for a broader audience!

Original Post on the Swift Open Source Slack

[Help & Discussion Wanted] Hello dear distributed folks,
I was taking a try to implement a distributed actor system based on an existing RPC protocol. The current mechanism works pretty well if we’re using Swift for both the client and the server, because a RemoteCallTarget holds a Swift-mangled function name that we can serialize and pass through the network.

Things are becoming tricky if we don’t want to carry such a string around (for the security sake or for interop with other languages / existing implementations). For the client site I managed to use swift_demangle to extract enough information from the mangled identifier, helping me to locate the actor class and function identifier. For the server site, however, I’ve got no idea on how to mangle it back to a valid RemoteCallTarget. I found Swift mangling rules hard to implement correctly (I tried to implement it in a macro but it turns out that the puny coding is really hard!!).

Given that what executeDistributedTarget does first is to demangle the identifier and to get the function type, I wonder if we can introduce a variant that directly takes a distributed func from the resolved actor. This works best with keypath, but sadly keypath doesn’t work with effective functions now, so we have to use a labeled function name + function signature (+ generic env & substitution), which can be combined with the actor instance to resolve the function.

Alternatively, if we have a way to get a mangled name and inject it into the runtime reflection metadata, things could be easier. I remembered Azoy did something similar for subcommand auto discovery in the argument parser, but that’s a lot of C boilerplate and largely overkill. A macro helper would be great, but since mangling happens after SIL we’re almost not possible to get this out during AST generation.

2 Likes

Thanks for the summary of your findings.

Overall I definitely agree that we should open up some form of customizability and versioning here. It's been on the roadmap for a while and it'd be good to find a good way to solve it all using one mechanisms.

The ABI breaking changes which do not impact remote calls is another thing I'd like to solve, like if your parameter type changes from class -> struct etc. Remote calls don't care about this since they don't pass directly but through encoding/decoding etc.

So detaching a bit from the manglings would be good.

And yes, the StableNames idea that was floated way back then is another potential idea here. And it's nice to see the erlang compat system using that idea like that.

I don't know if the keypaths idea is functional though -- either way then you'd have to encode that keypath which again would have potentially much information that's mangling based...

In other words, I think we need to make the mangling scheme used by a system customizable perhaps? You're right that only the compiler and runtime can really perform the proper mangling, but if you wanted to use a system with "simple names" (just the method names), we could do that BUT it would prevent having method overloads because we cannot distinguish them anymore based on the identifier.

I would like to hear specific examples you'd like to support in your system, and what exactly we'd mangle them as. For example, if we offered to opt into simple mangling like methodName ONLY and ban all overloads, this would be one possibility. Or we could have a @StableName that causes that.

We could also lean into @abi() and see if there's some "Wire ABI" concept we could lean into here... I'm also interested in allowing backwards compatible calls, where:

// "old version" (caller)
hello(name:)

// "new version" on recipient has gained new parameters
hello(name:age = 12 /*some default here*/)

which would allow the "new" method be called by a caller that is not aware of the defaulted age: parameter that has been rolled out to the new version on the server. Here again we could either use stable names, or "lock in" the ABI of the "base method" that is the mangling of hello(name:) and permit calling the hello(name:age:) if no age parameter was provided...

Let's list some specific examples of calls and tradeoffs we'd be okey supporting and then loop back to implementation details, wdyt?