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

[SR-5441] Closure Capture ABI #48015

Closed
atrick opened this issue Jul 12, 2017 · 10 comments
Closed

[SR-5441] Closure Capture ABI #48015

atrick opened this issue Jul 12, 2017 · 10 comments
Assignees
Labels
affects ABI Flag: Affects ABI compiler The Swift compiler in itself task

Comments

@atrick
Copy link
Member

atrick commented Jul 12, 2017

Previous ID SR-5441
Radar rdar://problem/33255593
Original Reporter @atrick
Type Task
Status Closed
Resolution Done
Additional Detail from JIRA
Votes 0
Component/s Compiler
Labels Task, AffectsABI
Assignee @aschwaighofer
Priority Medium

md5: 940b3fa562a38f5d53346475aff54fcc

relates to:

  • SR-5699 Non escaping blocks should not need to malloc captured values (stack memory pointers are just fine)
  • SR-6148 SIL Representation of Closures

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:

  1. the convention for passing `@thick` function types as arguments.
  2. the convention for passing the closure context.
  3. the convention for passing arguments to a closure given its context.

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):

  func takesEscapingClosure(_ f: @escaping ()->())
  -> sil takesEscapingClosure(@callee_guaranteed @escaping ()->()) -> ()
  -> takesEscapingClosure(void (*)(), BoxReference)

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.)

  func takesNonescapingClosure(_ f: ()->())
  -> sil takesNonescapingClosure(@callee_guaranteed ()->()) -> ()
  -> takesNonescapingClosure(void (*)(), OpaqueBox)

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`.

@atrick
Copy link
Member Author

atrick commented Jul 12, 2017

@swift-ci create

@aschwaighofer
Copy link
Member

Some more notes:

For ABI stability here is a solution that I had in mind:

  • introduce a @noescaping SILFunctionType
  • make the closure context of all thick functions @callee_guaranteed
  • retain/release of a "@noescaping @callee_guaranteed function" is a no-op
  • I also believe that we must always pass "@noescaping @calle_guaranteed functions” with a guaranteed convention. This is so that we can cast a "@escaping @callee_guaranteed function” to a “@noescaping @callee_guaranteed function” and pass it as a parameter to a function “use_closure” and at the same time are force to guaranteed the lifetime of the @escaping function across “use_closure”.

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
  • I would represent creation of a @noescaping closure with a partial_apply [stack]. This would follow the existing alloc_object [stack] / dealloc_ref [stack] pattern.

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 : $()
}

@atrick
Copy link
Member Author

atrick commented Oct 13, 2017

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.

@aschwaighofer
Copy link
Member

You just pointed out the difference yourself:

  • @callee_guaranteed vs @callee_owned describes what happens at closure invocation site

  • passing a closure @guaranteed vs @owned describes how you pass the value as an argument

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)

@aschwaighofer
Copy link
Member

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.

@atrick
Copy link
Member Author

atrick commented Oct 13, 2017

This is actually an important point that I just momentarily forgot. There are two cases in the ABI:

`@owned @escaping @callee_guaranteed`: ref-counted object

`@guaranteed @NoEscape @callee_guaranteed`: opaque word

When you get to that point, please specify the ABI rules so I don't keep forgetting.

@aschwaighofer
Copy link
Member

I'm sure you meant to say three cases for ABI:

  • `@owned @escaping @callee_guaranteed`: closure-fun-ptr, ref-counted object pair

  • `@guaranteed @escaping @callee_guaranteed`: closure-fun-ptr, ref-counted object pair
    For the usual reason when there is a ref-counted object.

  • `@guaranteed @NoEscape @callee_guaranteed`: closure-fun-ptr, opaque word pair
    From an ABI perspective, because retain/releases on @NoEscape closures are no-oops, @guaranteed == @owned, however at the SIL modeling level we want @guaranteed so that the lifetime of an argument is ensured across the call.

      %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 () -> ()
      

@atrick
Copy link
Member Author

atrick commented Oct 14, 2017

That's right. I simply meant there are two representations.

@atrick
Copy link
Member Author

atrick commented Mar 16, 2018

aschwaighofer@apple.com (JIRA User) can we close this now?

@aschwaighofer
Copy link
Member

Yes, the ABI work is done.

@swift-ci swift-ci transferred this issue from apple/swift-issues Apr 25, 2022
This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
affects ABI Flag: Affects ABI compiler The Swift compiler in itself task
Projects
None yet
Development

No branches or pull requests

2 participants