I'd like to understand the situations where we're actually inserting checks under this proposal. Let me lay out some structure for that discussion.
- Let's just call everything a function and ignore all the surface-level details like property accessors vs. methods. We've got code that requires isolation, it's getting called somehow, and that's all that matters.
- We only care about synchronous functions because
async
functions that require isolation will just establish it internally. And synchronous functions preserve isolation, so really we just care about establishing a precondition of isolation in the function. So we're talking about having a check on one side or the other of a call.
- We've got three basic options for how to do checking:
- We emit an unconditional check, which has code-size and execution-time costs in all cases.
- We create two paths for the call, one of which does a check, and then use the unchecked path when we know that we've safely established isolation in the caller. This mostly avoids execution-time costs but probably has significantly larger code-size costs (unless we can statically eliminate the checking path).
- We don't do the check and just accept dynamic unsafety.
The key question is how the call is done and what each side of the call knows about the other. Since we're protecting callees, the information question comes down to whether we know the caller can be trusted to isolate the function correctly. We can then consider that question for each of the basic ways we support for calling Swift functions:
- calls to known functions (direct dispatch)
- calls to opaque function values (arbitrary values of function type)
- calls through Objective-C dispatch (
@objc
)
- calls to protocol requirements
- calls to overridable class methods
Objective-C dispatch is highly dynamic by design. Moreover, we can only provide a single entry point in the method table for any particular method selector. Moreover, callers will very often be cross-language, and in particular they will be Objective-C, which we cannot easily make cooperate with Swift concurrency. Moreover, there's a significant baseline cost to being called through Objective-C dispatch, and it's not unreasonable to say that code sensitive to micro-level performance should try to avoid it anyway. So I think we should be doing this check in any Objective-C entry point we emit for an isolated method. And the proposal is clear that we should do that; so far, so good.
With function values, we again don't have the flexibility to emit different entry points — we're not going to carry around multiple function pointers in every function value. We do, however, have some flexibility to add logic around an existing function value. If the function value comes from a closure, we can usually just add that logic directly to the closure function. If the function value is opaque, or it's something like a direct function reference, we can add the logic to a "thunk" that wraps the old function value. So I think we should be adding this logic to any function value that we can't prove (to some reasonable extent) stays within trusted code. It sounds like the proposal is aiming to do that, but it'd be good to understand and document the limitations of that.
Known functions, protocol requirements, and overridable class methods are surprisingly similar to each other. First, we do have the ability to emit multiple entry points per source function by adding new public symbols, protocol requirements, and/or class v-table entries. Doing that implicitly and everywhere would be a large code-size hit. If we were designing everything from scratch, we could avoid duplicating entry points by just performing the checks on the caller side. Unfortunately, that requires cooperation from the caller side. If the caller is recompiled by a compiler aware of this feature, but in a language mode that doesn't generally enforce isolation, we could add checks to the caller; we'd then be able to eliminate those checks if/when the caller is upgraded to Swift 6. However, if the caller isn't recompiled, those checks just won't happen. Regardless, it sounds like there's nothing like this currently in the proposal, and we simply don't perform any checks for these kinds of dispatch except for the specific case of isolated witnesses to non-isolated protocol requirements.
Do I understand the proposal correctly?