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

Add a new parameter convention @in_cxx for non-trivial C++ classes that are passed indirectly and destructed by the caller #73019

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

ahatanaka
Copy link
Contributor

Fix a bug where @in, which indicates the callee is responsible for destroying the passed object, was being used to pass such classes.

rdar://122707697

@ahatanaka
Copy link
Contributor Author

@swift-ci please test

@ahatanaka
Copy link
Contributor Author

@swift-ci please test

@ahatanaka ahatanaka force-pushed the non-trivial-cxx-class-convention branch from c6c9427 to 4c26cbb Compare April 23, 2024 23:56
@ahatanaka
Copy link
Contributor Author

@swift-ci please test

@@ -347,6 +347,11 @@ public enum ArgumentConvention : CustomStringConvertible {
/// convention used by mutable captures in @noescape closures.
case indirectInoutAliasable

/// This argument is passed indirectly, i.e. by directly passing the address
/// of an object in memory. The callee may modify, but does not destroy the
/// object.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// object.
/// object. This corresponds to the parameter-passing convention of the
/// Itanium C++ ABI, which is used ubiquitously on non-Windows targets.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description sounds like either inout or inout_aliasable to me. What are the SIL-level semantics of the new convention?

@@ -416,7 +422,8 @@ public enum ArgumentConvention : CustomStringConvertible {
.packOwned, .packGuaranteed:
return true
case .directOwned, .directUnowned, .directGuaranteed,
.indirectInout, .indirectInoutAliasable, .indirectOut,
.indirectInout, .indirectInoutAliasable, .indirectInCXX,
.indirectOut,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what this property is used for, but if it includes both @in and @in_guaranteed, it probably ought to include @in_cxx.

@@ -164,7 +164,7 @@ public struct ParameterInfo : CustomStringConvertible {
switch convention {
case .indirectIn, .indirectInGuaranteed:
return hasLoweredAddresses || type.isOpenedExistentialWithError()
case .indirectInout, .indirectInoutAliasable:
case .indirectInout, .indirectInoutAliasable, .indirectInCXX:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like @gottesmm to weigh in here. Naively, I'd say it'd be nice to model these as direct arguments in non-lowered-addresses mode, but the fact that we have to destroy the value in the caller (and observe any mutations made by the callee) might make that impractical.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @atrick

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's potentially useful to have a SIL convention that says: the caller and callee must observe the same address of an addressable value. But that isn't generally implementable for all T given reabstraction of function types and tuples.

I'm still baffled by what this convention means. The comment says it's about observing mutation. If that's true, then we should pass a SIL address in non-lowered-addresses mode. But why, then aren't you using inout?

If it is not the case that the caller and callee need to observe the same address, then we should pass SIL values instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw an assertion failure when I tried using inout by making getIndirectCParameterConvention return Indirect_Inout instead of Indirect_In_CXX.

The failed assertion is in ArgEmitter::emit:

assert(arg.hasLValueType() == param.isIndirectInOut());

Copy link
Member

@rjmccall rjmccall Apr 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the Itanium C++ ABI convention in which non-trivial C++ values are passed indirectly but the caller is responsible for destroying them. The caller and callee do need to observe the same address. It is essentially the same as inout at the SIL level except that it's not inout in the source language, and I didn't want us to have to weaken all the SIL lowering assertions that e.g. an inout parameter always corresponds to an l-value argument expression, because those are quite useful in catching type-checking bugs. That is why I suggested to Akira that he add a new convention that was mostly treated the same way as inout throughout the SIL pipeline.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably pass SIL arguments to in-cxx by-address at all stages, and make it very clear that the caller can observe mutation.

Can the callee observe aliases?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameter object can't be directly named except through the parameter; it's never the same object as e.g. a local variable in the caller. However, it can be aliased in the same way that almost any C++ object technically can, e.g. by escaping this during construction. We've generally been assuming that that's not something we need to worry about, at least from the perspective of "well, we're obviously not going to be enforce exclusivity on types that don't follow basic local-reasoning principles".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It occurs to me that we probably need special logic when consuming parameters like this. They probably should be consumable as long as the type is movable; we should compile consumption as a C++ move and then enforce use restrictions on the moved-from value just like we would for a Swift value, even though technically the object is still there.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm starting to understand. At the source level, this is pass-by-value into the callee, and the caller has no visibility of that value. But, since the convention requires the caller to destroy the value, the caller's implementation must also observe any mutation.

Just like @in, In ownership SIL (OSSA) the argument should be consumed by the call (not by a separate destroy after the call).

Just like @in, non-address-lowered SIL should pass the value, not an address.

OwnershipModelEliminator could insert a destroy_addr after the call for these arguments. By then, they will be addresses. And IRGen doesn't need to do anything special.

@@ -4059,6 +4059,11 @@ enum class ParameterConvention : uint8_t {
/// convention used by mutable captures in @noescape closures.
Indirect_InoutAliasable,

/// This argument is passed indirectly, i.e. by directly passing the address
/// of an object in memory. The callee may modify, but does not destroy the
/// object.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// object.
/// object. This corresponds to the parameter-passing convention of the
/// Itanium C++ ABI, which is used ubiquitously on non-Windows targets.

@@ -200,6 +200,8 @@ struct SILDeclRef {
/// Set if this is for an async let closure.
unsigned isAsyncLetClosure : 1;

unsigned CFunctionPointer : 1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why this is necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to get getAbstractionPatternForConstant to return an AbstractionPattern with a clang type.

Without this change, checkForABIDifferences in convertCFunctionSignature returns TypeConverter::ABIDifference::NeedsThunk because loweredResultTy and loweredDestTy don't match, which causes the compiler to reject the code. loweredDestTy's parameter type is @in_cxx and I think loweredResultTy's parameter type would be @in_guaranteed without the change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran into this problem when I was testing testClosureToFuncPtr in closure-thunk-macosx.swift, which passes a swift closure to a C++ function taking a function pointer.

Copy link
Member

@rjmccall rjmccall Apr 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Conventions object for a foreign parameter of one of these types should always return @in_cxx, whether it appears as part of a C function pointer, a block, an ObjC method, or whatever. You should be able to just do this in getIndirectCParameterConvention.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was needed to fix a preexisting bug, but I think it should be fixed in a separate patch and it can be fixed without making any changes to SILDeclRef.

#73299

@@ -2160,6 +2160,7 @@ NodePointer Demangler::demangleImplParamConvention(Node::Kind ConvKind) {
case 'l': attr = "@inout"; break;
case 'b': attr = "@inout_aliasable"; break;
case 'n': attr = "@in_guaranteed"; break;
case 'C': attr = "@in_cxx"; break;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be documented in docs/ABI/Mangling.rst.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ahatanaka ahatanaka force-pushed the non-trivial-cxx-class-convention branch from 4c26cbb to 198dd06 Compare May 2, 2024 21:23
@ahatanaka
Copy link
Contributor Author

@swift-ci please test

@ahatanaka
Copy link
Contributor Author

The updated patch treats @in_cxx as consumed arguments just like @in (except that it can be mutated) and inserts destroy_addr right before IRGen.

// CHECK: call void @llvm.lifetime.start.p0(i64 8, ptr %[[V0]])
// CHECK: call ptr @__swift_cxx_ctor_ZN10NonTrivialC1Ev(ptr %[[V0]])
// CHECK: invoke void %[[V1]](ptr %[[V0]])
// CHECK: to label %[[INVOKE_CONT:.*]] unwind label %{{.*}}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can turn invoke into call and simplify the IR if we fix this bug: #73396

@@ -1416,7 +1421,11 @@ emitObjCThunkArguments(SILGenFunction &SGF, SILLocation loc, SILDeclRef thunk,
arg = emitObjCUnconsumedArgument(SGF, loc, arg);
}

auto managedArg = SGF.emitManagedRValueWithCleanup(arg);
// Don't emit a cleanup if the argument is @in_cxx.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to use @in_guaranteed instead of @in_cxx for thunk parameters so that we don't have to make this change, but ran into a few issues.

@ahatanaka ahatanaka force-pushed the non-trivial-cxx-class-convention branch from b9db498 to 4c4cc4f Compare May 3, 2024 06:13
@ahatanaka
Copy link
Contributor Author

@swift-ci please test

Copy link
Member

@atrick atrick left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not completely confident about the SILGen changes. Could you please make sure that any tests specific to this feature also work with -enable-sil-opaque-values. We're trying to enable that by default now and can't keep adding new problems to debug.

@@ -325,6 +325,10 @@ static ManagedValue emitManagedParameter(SILGenFunction &SGF, SILLocation loc,
return ManagedValue::forBorrowedAddressRValue(value);
}

case ParameterConvention::Indirect_In_CXX:
// Don't emit a cleanup if the parameter is @in_cxx.
return ManagedValue::forOwnedRValue(value, CleanupHandle::invalid());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nate-chandler could you take a quick peek at just the SILGen changes to see if the make sense to you?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as SIL opaque values goes, an easy thing to check would be whether the tests [added in this PR] crash out of SILGen with -enable-sil-opaque-values. Treating such a parameter as owned while suppressing its destruction seems to make sense given the problem being solved.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see crashes when I add -enable-sil-opaque-values.

It crashes in SILValueOwnershipChecker::checkFunctionArgWithoutLifetimeEndingUses with the error message Owned function parameter without life ending uses! when I compile closure-thunk-executable-macosx.swift.

This is the SIL that's causing the crash:

// thunk for @escaping @callee_guaranteed (@in_guaranteed NonTrivial) -> ()
sil shared [transparent] [serialized] [reabstraction_thunk] [ossa] @$sSo10NonTrivialVIegn_ABIeyBC_TR : $@convention(c) (@inout_aliasable @block_storage @callee_guaranteed (@in_guaranteed NonTrivial) -> (), @in_cxx NonTrivial) -> () {
// %0                                             // user: %2
// %1                                             // user: %5
bb0(%0 : $*@block_storage @callee_guaranteed (@in_guaranteed NonTrivial) -> (), %1 : @owned $NonTrivial):
  %2 = project_block_storage %0 : $*@block_storage @callee_guaranteed (@in_guaranteed NonTrivial) -> () // user: %3
  %3 = load [copy] %2 : $*@callee_guaranteed (@in_guaranteed NonTrivial) -> () // users: %8, %4
  %4 = begin_borrow %3 : $@callee_guaranteed (@in_guaranteed NonTrivial) -> () // users: %6, %5
  %5 = apply %4(%1) : $@callee_guaranteed (@in_guaranteed NonTrivial) -> ()
  end_borrow %4 : $@callee_guaranteed (@in_guaranteed NonTrivial) -> () // id: %6
  %7 = tuple ()                                   // user: %9
  destroy_value %3 : $@callee_guaranteed (@in_guaranteed NonTrivial) -> () // id: %8
  return %7 : $()                                 // id: %9
} // end sil function '$sSo10NonTrivialVIegn_ABIeyBC_TR'

This is a thunk function for the block.

It seems that the checker is complaining that the second argument of the thunk (@in_cxx NonTrivial) doesn't have lifetime-ending uses.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder changing the thunk's parameter to @in_guaranteed NonTrivial would fix the crash. Alternatively, maybe emitObjCThunkArguments should emit the cleanup instead of suppressing it and instead have destroy_addr removed before IRGen.

@ahatanaka
Copy link
Contributor Author

@swift-ci please test

2 similar comments
@ahatanaka
Copy link
Contributor Author

@swift-ci please test

@ahatanaka
Copy link
Contributor Author

@swift-ci please test

that are passed indirectly and destructed by the caller

Fix a bug where `@in`, which indicates the callee is responsible for
destroying the passed object, was being used to pass such classes.

rdar://122707697
- @in_cxx is handled the same way as @in in callers and @in_guaranteed
  in callees (except for mutation). Fix checks in various places
  accordingly.

- Emit copies of @in_cxx parameters and destroy them in thunks.
'C' is already taken by @convention(c).
@ahatanaka ahatanaka force-pushed the non-trivial-cxx-class-convention branch from 45a5dc2 to dab4b05 Compare May 29, 2024 19:01
@ahatanaka
Copy link
Contributor Author

@swift-ci please test

…ion if it's used by a function taking a @in_cxx parameter
@ahatanaka
Copy link
Contributor Author

@swift-ci please test

@ahatanaka
Copy link
Contributor Author

@swift-ci please test

@ahatanaka
Copy link
Contributor Author

@swift-ci please test

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c++ interop Feature: Interoperability with C++
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants