Skip to content

Commit

Permalink
Cleanup caller allocated and callee allocated resources separately (#…
Browse files Browse the repository at this point in the history
…89982)

This PR separates cleaning up caller allocated resources and callee allocated resources into separate stages in the managed to unmanaged direction. Caller allocated parameters (anything except 'out') will clean up the same way. Callee allocated parameters ('out' parameters) will be cleaned up only if the invocation succeeded.
  • Loading branch information
jtschuster authored Aug 8, 2023
1 parent f5889ec commit 191ec61
Show file tree
Hide file tree
Showing 22 changed files with 385 additions and 86 deletions.
21 changes: 13 additions & 8 deletions docs/design/libraries/LibraryImportGenerator/Pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,12 @@ The stub code generator itself will handle some initial setup and variable decla
- Call `Generate` on the marshalling generator for every parameter
1. `GuaranteedUnmarshal`: conversion of native to managed data even when an exception is thrown
- Call `Generate` on the marshalling generator for every parameter.
1. `Cleanup`: free any allocated resources
- If this stage has any statements, put them in an if statement where the condition represents whether the call succeeded
1. `CleanupCallerAllocated`: free any resources allocated by the caller
- Call `Generate` on the marshalling generator for every parameter
1. `CleanupCalleeAllocated`: if the native method succeeded, free any resources allocated by the callee (`out` parameters and return values)
- Call `Generate` on the marshalling generator for every parameter
- If this stage has any statements, put them in an if statement where the condition represents whether the call succeeded

Generated P/Invoke structure (if no code is generated for `GuaranteedUnmarshal` and `Cleanup`, the `try-finally` is omitted):
```C#
Expand All @@ -113,7 +117,8 @@ try
finally
{
<< GuaranteedUnmarshal >>
<< Cleanup >>
<< CleanupCalleeAllocated >>
<< CleanupCallerAllocated >>
}
```

Expand All @@ -138,12 +143,12 @@ Support for these features is indicated in code by the `abstract` `SingleFrameSp

The various scenarios mentioned above have different levels of support for these specialized features:

| Scenarios | Pinning and Stack allocation across the native context | Storing additional temporary state in locals |
|------|-----|-----|
| P/Invoke | supported | supported |
| Reverse P/Invoke | unsupported | supported |
| User-defined structure content marshalling | unsupported | unsupported |
| non-blittable array marshalling | unsupported | unuspported |
| Scenarios | Pinning and Stack allocation across the native context | Storing additional temporary state in locals |
|--------------------------------------------|--------------------------------------------------------|----------------------------------------------|
| P/Invoke | supported | supported |
| Reverse P/Invoke | unsupported | supported |
| User-defined structure content marshalling | unsupported | unsupported |
| non-blittable array marshalling | unsupported | unuspported |

To help enable developers to use the full model described in the [Struct Marshalling design](./StructMarshalling.md), we declare that in contexts where `AdditionalTemporaryStateLivesAcrossStages` is false, developers can still assume that state declared in the `Setup` phase is valid in any phase, but any side effects in code emitted in a phase other than `Setup` will not be guaranteed to be visible in other phases. This enables developers to still use the identifiers declared in the `Setup` phase in their other phases, but they'll need to take care to design their generators to handle these rules.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using Microsoft.CodeAnalysis;

namespace Microsoft.Interop.JavaScript
{
Expand Down Expand Up @@ -61,13 +61,13 @@ public BlockSyntax GenerateJSExportBody()
{
StatementSyntax invoke = InvokeSyntax();
GeneratedStatements statements = GeneratedStatements.Create(_marshallers, _context);
bool shouldInitializeVariables = !statements.GuaranteedUnmarshal.IsEmpty || !statements.Cleanup.IsEmpty;
bool shouldInitializeVariables = !statements.GuaranteedUnmarshal.IsEmpty || !statements.CleanupCallerAllocated.IsEmpty || !statements.CleanupCalleeAllocated.IsEmpty;
VariableDeclarations declarations = VariableDeclarations.GenerateDeclarationsForUnmanagedToManaged(_marshallers, _context, shouldInitializeVariables);

var setupStatements = new List<StatementSyntax>();
SetupSyntax(setupStatements);

if (!statements.GuaranteedUnmarshal.IsEmpty)
if (!(statements.GuaranteedUnmarshal.IsEmpty && statements.CleanupCalleeAllocated.IsEmpty))
{
setupStatements.Add(MarshallerHelpers.Declare(PredefinedType(Token(SyntaxKind.BoolKeyword)), InvokeSucceededIdentifier, initializeToDefault: true));
}
Expand All @@ -81,7 +81,7 @@ public BlockSyntax GenerateJSExportBody()

tryStatements.Add(invoke);

if (!statements.GuaranteedUnmarshal.IsEmpty)
if (!(statements.GuaranteedUnmarshal.IsEmpty && statements.CleanupCalleeAllocated.IsEmpty))
{
tryStatements.Add(ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression,
IdentifierName(InvokeSucceededIdentifier),
Expand All @@ -94,12 +94,12 @@ public BlockSyntax GenerateJSExportBody()

List<StatementSyntax> allStatements = setupStatements;
List<StatementSyntax> finallyStatements = new List<StatementSyntax>();
if (!statements.GuaranteedUnmarshal.IsEmpty)
if (!(statements.GuaranteedUnmarshal.IsEmpty && statements.CleanupCalleeAllocated.IsEmpty))
{
finallyStatements.Add(IfStatement(IdentifierName(InvokeSucceededIdentifier), Block(statements.GuaranteedUnmarshal)));
finallyStatements.Add(IfStatement(IdentifierName(InvokeSucceededIdentifier), Block(statements.GuaranteedUnmarshal.Concat(statements.CleanupCalleeAllocated))));
}

finallyStatements.AddRange(statements.Cleanup);
finallyStatements.AddRange(statements.CleanupCallerAllocated);
if (finallyStatements.Count > 0)
{
allStatements.Add(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,14 @@ public BlockSyntax GenerateJSImportBody()
{
StatementSyntax invoke = InvokeSyntax();
GeneratedStatements statements = GeneratedStatements.Create(_marshallers, _context);
bool shouldInitializeVariables = !statements.GuaranteedUnmarshal.IsEmpty || !statements.Cleanup.IsEmpty;
bool shouldInitializeVariables = !statements.GuaranteedUnmarshal.IsEmpty || !statements.CleanupCallerAllocated.IsEmpty || !statements.CleanupCalleeAllocated.IsEmpty;
VariableDeclarations declarations = VariableDeclarations.GenerateDeclarationsForManagedToUnmanaged(_marshallers, _context, shouldInitializeVariables);

var setupStatements = new List<StatementSyntax>();
BindSyntax(setupStatements);
SetupSyntax(setupStatements);

if (!statements.GuaranteedUnmarshal.IsEmpty)
if (!(statements.GuaranteedUnmarshal.IsEmpty && statements.CleanupCalleeAllocated.IsEmpty))
{
setupStatements.Add(MarshallerHelpers.Declare(PredefinedType(Token(SyntaxKind.BoolKeyword)), InvokeSucceededIdentifier, initializeToDefault: true));
}
Expand All @@ -88,7 +88,7 @@ public BlockSyntax GenerateJSImportBody()
tryStatements.AddRange(statements.PinnedMarshal);

tryStatements.Add(invoke);
if (!statements.GuaranteedUnmarshal.IsEmpty)
if (!(statements.GuaranteedUnmarshal.IsEmpty && statements.CleanupCalleeAllocated.IsEmpty))
{
tryStatements.Add(ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression,
IdentifierName(InvokeSucceededIdentifier),
Expand All @@ -100,12 +100,12 @@ public BlockSyntax GenerateJSImportBody()

List<StatementSyntax> allStatements = setupStatements;
List<StatementSyntax> finallyStatements = new List<StatementSyntax>();
if (!statements.GuaranteedUnmarshal.IsEmpty)
if (!(statements.GuaranteedUnmarshal.IsEmpty && statements.CleanupCalleeAllocated.IsEmpty))
{
finallyStatements.Add(IfStatement(IdentifierName(InvokeSucceededIdentifier), Block(statements.GuaranteedUnmarshal)));
finallyStatements.Add(IfStatement(IdentifierName(InvokeSucceededIdentifier), Block(statements.GuaranteedUnmarshal.Concat(statements.CleanupCalleeAllocated))));
}

finallyStatements.AddRange(statements.Cleanup);
finallyStatements.AddRange(statements.CleanupCallerAllocated);
if (finallyStatements.Count > 0)
{
// Add try-finally block if there are any statements in the finally block
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public BlockSyntax GenerateStubBody(int index, ImmutableArray<FunctionPointerUnm
BracketedArgumentList(SingletonSeparatedList(
Argument(LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(index)))))),
callConv));
bool shouldInitializeVariables = !statements.GuaranteedUnmarshal.IsEmpty || !statements.Cleanup.IsEmpty;
bool shouldInitializeVariables = !statements.GuaranteedUnmarshal.IsEmpty || !statements.CleanupCallerAllocated.IsEmpty || !statements.CleanupCalleeAllocated.IsEmpty;
VariableDeclarations declarations = VariableDeclarations.GenerateDeclarationsForManagedToUnmanaged(_marshallers, _context, shouldInitializeVariables);

if (_setLastError)
Expand All @@ -143,7 +143,7 @@ public BlockSyntax GenerateStubBody(int index, ImmutableArray<FunctionPointerUnm
initializeToDefault: false));
}

if (!statements.GuaranteedUnmarshal.IsEmpty)
if (!(statements.GuaranteedUnmarshal.IsEmpty && statements.CleanupCalleeAllocated.IsEmpty))
{
setupStatements.Add(MarshallerHelpers.Declare(PredefinedType(Token(SyntaxKind.BoolKeyword)), InvokeSucceededIdentifier, initializeToDefault: true));
}
Expand Down Expand Up @@ -174,7 +174,7 @@ public BlockSyntax GenerateStubBody(int index, ImmutableArray<FunctionPointerUnm
tryStatements.AddRange(statements.NotifyForSuccessfulInvoke);

// <invokeSucceeded> = true;
if (!statements.GuaranteedUnmarshal.IsEmpty)
if (!(statements.GuaranteedUnmarshal.IsEmpty && statements.CleanupCalleeAllocated.IsEmpty))
{
tryStatements.Add(ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression,
IdentifierName(InvokeSucceededIdentifier),
Expand All @@ -197,12 +197,12 @@ public BlockSyntax GenerateStubBody(int index, ImmutableArray<FunctionPointerUnm

List<StatementSyntax> allStatements = setupStatements;
List<StatementSyntax> finallyStatements = new List<StatementSyntax>();
if (!statements.GuaranteedUnmarshal.IsEmpty)
if (!(statements.GuaranteedUnmarshal.IsEmpty && statements.CleanupCalleeAllocated.IsEmpty))
{
finallyStatements.Add(IfStatement(IdentifierName(InvokeSucceededIdentifier), Block(statements.GuaranteedUnmarshal)));
finallyStatements.Add(IfStatement(IdentifierName(InvokeSucceededIdentifier), Block(statements.GuaranteedUnmarshal.Concat(statements.CleanupCalleeAllocated))));
}

finallyStatements.AddRange(statements.Cleanup);
finallyStatements.AddRange(statements.CleanupCallerAllocated);
if (finallyStatements.Count > 0)
{
// Add try-finally block if there are any statements in the finally block
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public IEnumerable<StatementSyntax> Generate(TypePositionInfo info, StubCodeCont
{
Debug.Assert(info.MarshallingAttributeInfo is ManagedHResultExceptionMarshallingInfo);

if (context.CurrentStage != StubCodeContext.Stage.Unmarshal)
if (context.CurrentStage != StubCodeContext.Stage.NotifyForSuccessfulInvoke)
{
yield break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
Expand Down Expand Up @@ -52,9 +53,11 @@ public BlockSyntax GenerateStubBody(ExpressionSyntax methodToInvoke)
_marshallers,
_context,
methodToInvoke);
Debug.Assert(statements.CleanupCalleeAllocated.IsEmpty);

bool shouldInitializeVariables =
!statements.GuaranteedUnmarshal.IsEmpty
|| !statements.Cleanup.IsEmpty
|| !statements.CleanupCallerAllocated.IsEmpty
|| !statements.ManagedExceptionCatchClauses.IsEmpty;
VariableDeclarations declarations = VariableDeclarations.GenerateDeclarationsForUnmanagedToManaged(_marshallers, _context, shouldInitializeVariables);

Expand All @@ -77,7 +80,7 @@ public BlockSyntax GenerateStubBody(ExpressionSyntax methodToInvoke)

SyntaxList<CatchClauseSyntax> catchClauses = List(statements.ManagedExceptionCatchClauses);

finallyStatements.AddRange(statements.Cleanup);
finallyStatements.AddRange(statements.CleanupCallerAllocated);
if (finallyStatements.Count > 0)
{
allStatements.Add(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis.CSharp;
Expand Down Expand Up @@ -107,7 +108,7 @@ public PInvokeStubCodeGenerator(
public BlockSyntax GeneratePInvokeBody(string dllImportName)
{
GeneratedStatements statements = GeneratedStatements.Create(_marshallers, _context, IdentifierName(dllImportName));
bool shouldInitializeVariables = !statements.GuaranteedUnmarshal.IsEmpty || !statements.Cleanup.IsEmpty;
bool shouldInitializeVariables = !statements.GuaranteedUnmarshal.IsEmpty || !statements.CleanupCallerAllocated.IsEmpty || !statements.CleanupCalleeAllocated.IsEmpty;
VariableDeclarations declarations = VariableDeclarations.GenerateDeclarationsForManagedToUnmanaged(_marshallers, _context, shouldInitializeVariables);

var setupStatements = new List<StatementSyntax>();
Expand All @@ -121,7 +122,7 @@ public BlockSyntax GeneratePInvokeBody(string dllImportName)
initializeToDefault: false));
}

if (!statements.GuaranteedUnmarshal.IsEmpty)
if (!(statements.GuaranteedUnmarshal.IsEmpty && statements.CleanupCalleeAllocated.IsEmpty))
{
setupStatements.Add(MarshallerHelpers.Declare(PredefinedType(Token(SyntaxKind.BoolKeyword)), InvokeSucceededIdentifier, initializeToDefault: true));
}
Expand All @@ -148,7 +149,7 @@ public BlockSyntax GeneratePInvokeBody(string dllImportName)
}
tryStatements.Add(statements.Pin.NestFixedStatements(fixedBlock));
// <invokeSucceeded> = true;
if (!statements.GuaranteedUnmarshal.IsEmpty)
if (!(statements.GuaranteedUnmarshal.IsEmpty && statements.CleanupCalleeAllocated.IsEmpty))
{
tryStatements.Add(ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression,
IdentifierName(InvokeSucceededIdentifier),
Expand All @@ -160,12 +161,12 @@ public BlockSyntax GeneratePInvokeBody(string dllImportName)

List<StatementSyntax> allStatements = setupStatements;
List<StatementSyntax> finallyStatements = new List<StatementSyntax>();
if (!statements.GuaranteedUnmarshal.IsEmpty)
if (!(statements.GuaranteedUnmarshal.IsEmpty && statements.CleanupCalleeAllocated.IsEmpty))
{
finallyStatements.Add(IfStatement(IdentifierName(InvokeSucceededIdentifier), Block(statements.GuaranteedUnmarshal)));
finallyStatements.Add(IfStatement(IdentifierName(InvokeSucceededIdentifier), Block(statements.GuaranteedUnmarshal.Concat(statements.CleanupCalleeAllocated))));
}

finallyStatements.AddRange(statements.Cleanup);
finallyStatements.AddRange(statements.CleanupCallerAllocated);
if (finallyStatements.Count > 0)
{
// Add try-finally block if there are any statements in the finally block
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ public struct GeneratedStatements
public ImmutableArray<StatementSyntax> Unmarshal { get; init; }
public ImmutableArray<StatementSyntax> NotifyForSuccessfulInvoke { get; init; }
public ImmutableArray<StatementSyntax> GuaranteedUnmarshal { get; init; }
public ImmutableArray<StatementSyntax> Cleanup { get; init; }
public ImmutableArray<StatementSyntax> CleanupCallerAllocated { get; init; }
public ImmutableArray<StatementSyntax> CleanupCalleeAllocated { get; init; }

public ImmutableArray<CatchClauseSyntax> ManagedExceptionCatchClauses { get; init; }

Expand All @@ -38,7 +39,8 @@ public static GeneratedStatements Create(BoundGenerators marshallers, StubCodeCo
.AddRange(GenerateStatementsForStubContext(marshallers, context with { CurrentStage = StubCodeContext.Stage.Unmarshal })),
NotifyForSuccessfulInvoke = GenerateStatementsForStubContext(marshallers, context with { CurrentStage = StubCodeContext.Stage.NotifyForSuccessfulInvoke }),
GuaranteedUnmarshal = GenerateStatementsForStubContext(marshallers, context with { CurrentStage = StubCodeContext.Stage.GuaranteedUnmarshal }),
Cleanup = GenerateStatementsForStubContext(marshallers, context with { CurrentStage = StubCodeContext.Stage.Cleanup }),
CleanupCallerAllocated = GenerateStatementsForStubContext(marshallers, context with { CurrentStage = StubCodeContext.Stage.CleanupCallerAllocated }),
CleanupCalleeAllocated = GenerateStatementsForStubContext(marshallers, context with { CurrentStage = StubCodeContext.Stage.CleanupCalleeAllocated }),
ManagedExceptionCatchClauses = GenerateCatchClauseForManagedException(marshallers, context)
};
}
Expand Down Expand Up @@ -182,7 +184,8 @@ private static SyntaxTriviaList GenerateStageTrivia(StubCodeContext.Stage stage)
StubCodeContext.Stage.Invoke => "Call the P/Invoke.",
StubCodeContext.Stage.UnmarshalCapture => "Capture the native data into marshaller instances in case conversion to managed data throws an exception.",
StubCodeContext.Stage.Unmarshal => "Convert native data to managed data.",
StubCodeContext.Stage.Cleanup => "Perform required cleanup.",
StubCodeContext.Stage.CleanupCallerAllocated => "Perform cleanup of caller allocated resources.",
StubCodeContext.Stage.CleanupCalleeAllocated => "Perform cleanup of callee allocated resources.",
StubCodeContext.Stage.NotifyForSuccessfulInvoke => "Keep alive any managed objects that need to stay alive across the call.",
StubCodeContext.Stage.GuaranteedUnmarshal => "Convert native data to managed data even in the case of an exception during the non-cleanup phases.",
_ => throw new ArgumentOutOfRangeException(nameof(stage))
Expand Down
Loading

0 comments on commit 191ec61

Please sign in to comment.