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
[SR-5441] Closure Capture ABI #48015
Comments
@swift-ci create |
Some more notes: For ABI stability here is a solution that I had in mind:
Example: %closure = partial_apply %closure_fun(%capture) // heap
%borrowed_closure = begin_borrow %closure : @escaping @callee_guaranteed () -> ()
%noescaping_closure = convert_function %closure to @nonescaping @callee_guaranteed () -> ()
apply %use_closure (%noescaping_clsoure) : (@guaranteed @callee_guaranteed @nonescaping () -> ()) -> ()
end_borrow %closure
release %closure
Example: public class A {
public func doIt() { print("Hello world") }
}
public func use_closure(_ closure: () -> ()) {
closure()
}
public func noescaping(_ a: A, _ a1: A) {
let closure = {
a.doIt()
a1.doIt()
}
use_closure(closure)
}
sil @noescaping : $@convention(thin) (@owned A, @owned A) -> () {
bb0(%0 : $A, %1 : $A):
// function_ref closure #​1 in noescaping(_:_:)
%4 = function_ref @_T011TestCapture10noescapingyAA1AC_ADtFyycfU_ : $@convention(thin) (@owned A, @owned A) -> ()
%5 = copy_value %0 : $A
%6 = copy_value %1 : $A
%7 = partial_apply [stack] %4(%5, %6) : $@convention(thin) (@owned A, @owned A) -> () // creates a stack object similar to the existing partial apply closure box except it is not reference counted
%9 = function_ref @_T011TestCapture11use_closureyyycF : $@convention(thin) (@guaranteed @noescaping @callee_guaranteed () -> ()) -> ()
%10 = begin_borrow %7 : $@callee_owned () -> () // users: %13, %11
%12 = apply %9(%10) : $@convention(thin) (@guaranteed @noescaping @callee_owned () -> ()) -> ()
end_borrow %10 from %7 : $@noescaping @callee_guaranteed () -> (), $@noescaping @callee_guaranteed () -> ()
destroy_value %7 : $@noescaping @callee_guaranteed () -> ()// runtime no-op
dealloc_ref [stack] %7 : $@noescaping @callee_guaranteed () -> () // would destruct the objects captured by the closure
destroy_value %1 : $A
destroy_value %0 : $A %17 = tuple ()
return %17 : $()
}
sil @use_an_escaping : $@convention(thin) (@owned A, @owned A) -> () {
bb0(%0 : $A, %1 : $A):
// function_ref closure #​1 in noescaping(_:_:)
%4 = function_ref @_T011TestCapture10noescapingyAA1AC_ADtFyycfU_ : $@convention(thin) (@owned A, @owned A) -> ()
%5 = copy_value %0 : $A
%6 = copy_value %1 : $A
%7 = partial_apply %4(%5, %6) : $@convention(thin) (@owned A, @owned A) -> ()
%8 = begin_borrow %7: @escaping @callee_guaranteed () -> ()
%9 = convert_function %8 to @noescaping @callee_guaranteed () -> ()
%10 = function_ref @use : $@convention(thin) (@guaranteed @noescaping @callee_guaranteed () -> ()) -> ()
%12 = apply %10(%9) : $@convention(thin) (@guaranteed @noescaping @callee_owned () -> ()) -> ()
end_borrow %8 from %7 : $@noescaping @callee_guaranteed () -> (), $@noescaping @callee_guaranteed () -> ()
destroy_value %7: @escaping @callee_guaranteed () -> () // must live until here
destroy_value %1 : $A
destroy_value %0 : $A %17 = tuple ()
return %17 : $()
} |
aschwaighofer@apple.com (JIRA User), I'm not sure what the difference is between (a) make the closure context of all thick functions @callee_guaranteed and (b) pass "@noescaping @callee_guaranteed functions” with a guaranteed convention I understand that (a) refers to closure invocation while (b) refers to passing a closure as an argument. But in both cases we're just talking about the representation of the closure context itself. Just clarifying. |
You just pointed out the difference yourself:
It can still makes sense to pass a (escaping) closure @owned in say a setter. So for @escaping closures we will continue to pass them @owned or @guaranteed. However, we want @noescaping closures to always be passes @guaranteed to force the lifetime of a converted @escaping closure across the call that we pass it to (see the example above) |
This is purely a SIL modeling issue. Since whether we pass @noescaping closure as a @guaranteed vs @owned parameter makes no difference because the context is not reference counted i.e from an ABI perspective they are the same. |
I'm sure you meant to say three cases for ABI:
|
That's right. I simply meant there are two representations. |
aschwaighofer@apple.com (JIRA User) can we close this now? |
Yes, the ABI work is done. |
Additional Detail from JIRA
md5: 940b3fa562a38f5d53346475aff54fcc
relates to:
Issue Description:
This is a task to formally specify and implement a stable ABI for closure captures.
Summary of proposed ABI changes from the current design
1. Move from @callee_owned to @callee_guaranteed as the default.
2. Add a non-escaping case to the ABI that treats the context as an
opaque word instead of a reference counted box.
Background
When we talk about closure ABI, we're talking about `@thick` function
types in SIL. There will be two conventions for passing these function
types: escaping and non-escaping. In both cases, they are passed as a
function pointer and a context.
We need to specify:
We do not need to specify the layout of "boxed" arguments since the
closure body is always emitted within the same module that captures
the arguments.
Proposal
An escaping function is passed as a function pointer along with a
reference-counted context (called BoxReference below):
A single box provides access to all captured variables. This is
consistent with the current implementation.
A non-escaping function is passed as a function pointer along with a
word-sized opaque value represnting the context. (In practice, the
context will likely be an address directly into the Boxed captures.)
This differs from the current implementation by removing the guarantee
that the context can be reference counted. The callee is no longer
allowed to copy the box that represents the context.
To allow conversion between function types, we must change the
context's ownership convention from `@owned` to `@guaranteed`.
Non-ABI Implementation Details.
This ABI allows for easy conversion between function types. The layout
of the Box will be the same in both escaping and non-escaping
cases. The only difference in represention and semantics of the
context is that in the escaping case, the context may be retained by
the callee.
Converting from escaping to non-escaping:
The escaping function type must be retained for the duration of the
non-escaping variable. Within that scope, the address of the box can
simply be projected directly from the heap box and passed as the
context of a non-escaping closure.
Converting from non-escaping to escaping (withoutActuallyEscaping):
A heap box is allocated and the contents of the non-escaping box are
copied into the heap box and written back into the non-escaping box at
the end of scope. According to exclusivity rules, the closure can
neither escape nor be invoked nonreentrantly. The heap box will need
some additional state to enforce `withoutActuallyEscaping`.
The text was updated successfully, but these errors were encountered: