diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitDecl.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitDecl.cs
index 5b3af0ed..8e8a6633 100644
--- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitDecl.cs
+++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitDecl.cs
@@ -666,6 +666,16 @@ private void VisitFunctionDecl(FunctionDecl functionDecl)
var returnType = functionDecl.ReturnType;
var returnTypeName = GetRemappedTypeName(functionDecl, cxxRecordDecl, returnType, out var nativeTypeName);
+ var isCompoundAssignmentOperator = IsLatestCompoundAssignmentOperator(functionDecl, out var compoundAssignmentOperatorToken);
+
+ if (isCompoundAssignmentOperator)
+ {
+ // Project onto a C# 14 user-defined compound-assignment operator; the trailing
+ // `return this`/`*this` is dropped when the body is emitted below.
+ escapedName = $"operator {compoundAssignmentOperatorToken}";
+ returnTypeName = "void";
+ }
+
if (isManualImport && !_config.WithClasses.ContainsKey(name))
{
var parameters = functionDecl.Parameters;
@@ -690,11 +700,11 @@ private void VisitFunctionDecl(FunctionDecl functionDecl)
entryPoint = functionDecl.IsExternC ? GetCursorName(functionDecl) : functionDecl.Handle.Mangling.CString;
}
- var needsReturnFixup = (cxxMethodDecl is not null) && NeedsReturnFixup(cxxMethodDecl);
+ var needsReturnFixup = !isCompoundAssignmentOperator && (cxxMethodDecl is not null) && NeedsReturnFixup(cxxMethodDecl);
var desc = new FunctionOrDelegateDesc {
AccessSpecifier = accessSpecifier,
- NativeTypeName = nativeTypeName,
+ NativeTypeName = isCompoundAssignmentOperator ? null! : nativeTypeName,
EscapedName = escapedName,
ParentName = parentName,
EntryPoint = entryPoint,
@@ -880,7 +890,17 @@ private void VisitFunctionDecl(FunctionDecl functionDecl)
var currentContext = _context.AddLast((compoundStmt, null));
_outputBuilder.BeginConstructorInitializers();
- VisitStmts(compoundStmt.Body);
+
+ if (isCompoundAssignmentOperator)
+ {
+ // Drop the trailing `return this`/`*this`; the C# operator returns void.
+ VisitStmts([.. compoundStmt.Body.SkipLast(1)]);
+ }
+ else
+ {
+ VisitStmts(compoundStmt.Body);
+ }
+
_outputBuilder.EndConstructorInitializers();
Debug.Assert(_context.Last == currentContext);
@@ -1841,7 +1861,13 @@ private bool IsConstant(string targetTypeName, Expr initExpr)
}
// case CX_StmtClass_ChooseExpr:
- // case CX_StmtClass_CompoundLiteralExpr:
+
+ case CX_StmtClass_CompoundLiteralExpr:
+ {
+ var compoundLiteralExpr = (CompoundLiteralExpr)initExpr;
+ return IsConstant(targetTypeName, compoundLiteralExpr.Initializer);
+ }
+
// case CX_StmtClass_ConceptSpecializationExpr:
// case CX_StmtClass_ConvertVectorExpr:
// case CX_StmtClass_CoawaitExpr:
diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitStmt.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitStmt.cs
index 133c0800..99d5eb64 100644
--- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitStmt.cs
+++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitStmt.cs
@@ -1570,6 +1570,13 @@ private void VisitImplicitValueInitExpr(ImplicitValueInitExpr implicitValueInitE
StopCSharpCode();
}
+ private void VisitCompoundLiteralExpr(CompoundLiteralExpr compoundLiteralExpr)
+ {
+ // A C compound literal `(T){ ... }` has no distinct C# spelling; it projects as the
+ // object/collection initializer emitted by its inner initializer list.
+ Visit(compoundLiteralExpr.Initializer);
+ }
+
private void VisitInitListExpr(InitListExpr initListExpr)
{
var outputBuilder = StartCSharpCode();
@@ -2736,7 +2743,13 @@ private void VisitStmt(Stmt stmt)
}
// case CX_StmtClass_ChooseExpr:
- // case CX_StmtClass_CompoundLiteralExpr:
+
+ case CX_StmtClass_CompoundLiteralExpr:
+ {
+ VisitCompoundLiteralExpr((CompoundLiteralExpr)stmt);
+ break;
+ }
+
// case CX_StmtClass_ConvertVectorExpr:
// case CX_StmtClass_CoawaitExpr:
// case CX_StmtClass_CoyieldExpr:
diff --git a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.cs b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.cs
index 330a51e0..d9d1140a 100644
--- a/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.cs
+++ b/sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.cs
@@ -2311,6 +2311,69 @@ private bool TryRemapOperatorName(ref string name, FunctionDecl functionDecl)
return true;
}
+ // C++ compound-assignment operators have no friendly C# projection, so they are
+ // emitted under their .NET metadata names, consistent with `operator=` above.
+
+ case "operator+=":
+ {
+ name = "op_AdditionAssignment";
+ return true;
+ }
+
+ case "operator-=":
+ {
+ name = "op_SubtractionAssignment";
+ return true;
+ }
+
+ case "operator*=":
+ {
+ name = "op_MultiplicationAssignment";
+ return true;
+ }
+
+ case "operator/=":
+ {
+ name = "op_DivisionAssignment";
+ return true;
+ }
+
+ case "operator%=":
+ {
+ name = "op_ModulusAssignment";
+ return true;
+ }
+
+ case "operator&=":
+ {
+ name = "op_BitwiseAndAssignment";
+ return true;
+ }
+
+ case "operator|=":
+ {
+ name = "op_BitwiseOrAssignment";
+ return true;
+ }
+
+ case "operator^=":
+ {
+ name = "op_ExclusiveOrAssignment";
+ return true;
+ }
+
+ case "operator<<=":
+ {
+ name = "op_LeftShiftAssignment";
+ return true;
+ }
+
+ case "operator>>=":
+ {
+ name = "op_RightShiftAssignment";
+ return true;
+ }
+
default:
{
return false;
@@ -2318,6 +2381,76 @@ private bool TryRemapOperatorName(ref string name, FunctionDecl functionDecl)
}
}
+ private bool IsLatestCompoundAssignmentOperator(FunctionDecl functionDecl, [NotNullWhen(true)] out string? operatorToken)
+ {
+ operatorToken = null;
+
+ // C# 14 lets a compound-assignment operator be user-defined as an instance member with a
+ // void return that mutates in place. The C++ operator can project onto it only when it
+ // returns a pointer/reference to the declaring type and its body just returns `this`/`*this`,
+ // which is then dropped (see https://github.com/dotnet/ClangSharp/issues/821).
+ if (!_config.GenerateLatestCode || functionDecl is not CXXMethodDecl cxxMethodDecl || !cxxMethodDecl.IsInstance)
+ {
+ return false;
+ }
+
+ var parent = cxxMethodDecl.Parent;
+
+ if (parent is null)
+ {
+ return false;
+ }
+
+ var name = GetCursorName(functionDecl);
+
+ operatorToken = name switch {
+ "operator+=" or "operator-=" or "operator*=" or "operator/=" or "operator%=" or
+ "operator&=" or "operator|=" or "operator^=" or "operator<<=" or "operator>>=" => name["operator".Length..],
+ _ => null,
+ };
+
+ if (operatorToken is null)
+ {
+ return false;
+ }
+
+ var returnType = functionDecl.ReturnType.CanonicalType;
+
+ if (returnType is not (PointerType or ReferenceType) || (returnType.PointeeType.CanonicalType.AsCXXRecordDecl != parent))
+ {
+ operatorToken = null;
+ return false;
+ }
+
+ if (functionDecl.Body is not CompoundStmt compoundStmt || compoundStmt.BodyBack is not ReturnStmt returnStmt)
+ {
+ operatorToken = null;
+ return false;
+ }
+
+ var retValue = returnStmt.RetValue;
+
+ if (retValue is null || !ReturnsThis(retValue))
+ {
+ operatorToken = null;
+ return false;
+ }
+
+ return true;
+
+ static bool ReturnsThis(Expr retValue)
+ {
+ var expr = retValue.IgnoreParenCasts;
+
+ if (expr is UnaryOperator unaryOperator && (unaryOperator.Opcode == CXUnaryOperator_Deref))
+ {
+ expr = unaryOperator.SubExpr.IgnoreParenCasts;
+ }
+
+ return expr is CXXThisExpr;
+ }
+ }
+
private void UncheckStmt(string targetTypeName, Stmt stmt)
{
Debug.Assert(_outputBuilder is not null);
diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/C/CompoundLiteralExprTest.CSharp.Latest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/C/CompoundLiteralExprTest.CSharp.Latest.cs
new file mode 100644
index 00000000..bba464c8
--- /dev/null
+++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/C/CompoundLiteralExprTest.CSharp.Latest.cs
@@ -0,0 +1,25 @@
+namespace ClangSharp.Test
+{
+ public partial struct b3Vec3
+ {
+ public float x;
+
+ public float y;
+
+ public float z;
+ }
+
+ public static partial class Methods
+ {
+ [return: NativeTypeName("struct b3Vec3")]
+ public static b3Vec3 b3Negate([NativeTypeName("struct b3Vec3")] b3Vec3 a)
+ {
+ return new b3Vec3
+ {
+ x = -a.x,
+ y = -a.y,
+ z = -a.z,
+ };
+ }
+ }
+}
diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.CSharp.Compatible.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.CSharp.Compatible.cs
new file mode 100644
index 00000000..f11dc0e4
--- /dev/null
+++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.CSharp.Compatible.cs
@@ -0,0 +1,30 @@
+using System.Runtime.CompilerServices;
+
+namespace ClangSharp.Test
+{
+ public unsafe partial struct MyStruct
+ {
+ public int value;
+
+ [return: NativeTypeName("MyStruct &")]
+ public MyStruct* op_AdditionAssignment(MyStruct rhs)
+ {
+ value += rhs.value;
+ return (MyStruct*)Unsafe.AsPointer(ref this);
+ }
+
+ [return: NativeTypeName("MyStruct &")]
+ public MyStruct* op_MultiplicationAssignment(int scale)
+ {
+ value *= scale;
+ return (MyStruct*)Unsafe.AsPointer(ref this);
+ }
+
+ [return: NativeTypeName("MyStruct &")]
+ public MyStruct* op_DivisionAssignment([NativeTypeName("MyStruct &")] MyStruct* other)
+ {
+ value /= other->value;
+ return other;
+ }
+ }
+}
diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.CSharp.Default.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.CSharp.Default.cs
new file mode 100644
index 00000000..f11dc0e4
--- /dev/null
+++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.CSharp.Default.cs
@@ -0,0 +1,30 @@
+using System.Runtime.CompilerServices;
+
+namespace ClangSharp.Test
+{
+ public unsafe partial struct MyStruct
+ {
+ public int value;
+
+ [return: NativeTypeName("MyStruct &")]
+ public MyStruct* op_AdditionAssignment(MyStruct rhs)
+ {
+ value += rhs.value;
+ return (MyStruct*)Unsafe.AsPointer(ref this);
+ }
+
+ [return: NativeTypeName("MyStruct &")]
+ public MyStruct* op_MultiplicationAssignment(int scale)
+ {
+ value *= scale;
+ return (MyStruct*)Unsafe.AsPointer(ref this);
+ }
+
+ [return: NativeTypeName("MyStruct &")]
+ public MyStruct* op_DivisionAssignment([NativeTypeName("MyStruct &")] MyStruct* other)
+ {
+ value /= other->value;
+ return other;
+ }
+ }
+}
diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.CSharp.Latest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.CSharp.Latest.cs
new file mode 100644
index 00000000..4c898971
--- /dev/null
+++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.CSharp.Latest.cs
@@ -0,0 +1,24 @@
+namespace ClangSharp.Test
+{
+ public unsafe partial struct MyStruct
+ {
+ public int value;
+
+ public void operator +=(MyStruct rhs)
+ {
+ value += rhs.value;
+ }
+
+ public void operator *=(int scale)
+ {
+ value *= scale;
+ }
+
+ [return: NativeTypeName("MyStruct &")]
+ public MyStruct* op_DivisionAssignment([NativeTypeName("MyStruct &")] MyStruct* other)
+ {
+ value /= other->value;
+ return other;
+ }
+ }
+}
diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.CSharp.Preview.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.CSharp.Preview.cs
new file mode 100644
index 00000000..4c898971
--- /dev/null
+++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.CSharp.Preview.cs
@@ -0,0 +1,24 @@
+namespace ClangSharp.Test
+{
+ public unsafe partial struct MyStruct
+ {
+ public int value;
+
+ public void operator +=(MyStruct rhs)
+ {
+ value += rhs.value;
+ }
+
+ public void operator *=(int scale)
+ {
+ value *= scale;
+ }
+
+ [return: NativeTypeName("MyStruct &")]
+ public MyStruct* op_DivisionAssignment([NativeTypeName("MyStruct &")] MyStruct* other)
+ {
+ value /= other->value;
+ return other;
+ }
+ }
+}
diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.Xml.Compatible.xml b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.Xml.Compatible.xml
new file mode 100644
index 00000000..55fca8a3
--- /dev/null
+++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.Xml.Compatible.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+ int
+
+
+ MyStruct*
+
+ MyStruct
+
+ value += rhs.value;
+ return (MyStruct*)Unsafe.AsPointer(ref this);
+
+
+ MyStruct*
+
+ int
+
+ value *= scale;
+ return (MyStruct*)Unsafe.AsPointer(ref this);
+
+
+ MyStruct*
+
+ MyStruct*
+
+ value /= other->value;
+ return other;
+
+
+
+
diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.Xml.Default.xml b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.Xml.Default.xml
new file mode 100644
index 00000000..55fca8a3
--- /dev/null
+++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.Xml.Default.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+ int
+
+
+ MyStruct*
+
+ MyStruct
+
+ value += rhs.value;
+ return (MyStruct*)Unsafe.AsPointer(ref this);
+
+
+ MyStruct*
+
+ int
+
+ value *= scale;
+ return (MyStruct*)Unsafe.AsPointer(ref this);
+
+
+ MyStruct*
+
+ MyStruct*
+
+ value /= other->value;
+ return other;
+
+
+
+
diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.Xml.Latest.xml b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.Xml.Latest.xml
new file mode 100644
index 00000000..f5d26d76
--- /dev/null
+++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.Xml.Latest.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+ int
+
+
+ void
+
+ MyStruct
+
+ value += rhs.value;
+
+
+ void
+
+ int
+
+ value *= scale;
+
+
+ MyStruct*
+
+ MyStruct*
+
+ value /= other->value;
+ return other;
+
+
+
+
diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.Xml.Preview.xml b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.Xml.Preview.xml
new file mode 100644
index 00000000..f5d26d76
--- /dev/null
+++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/Baselines/CXXMethodDeclaration/CompoundAssignmentOperatorTest.Xml.Preview.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+ int
+
+
+ void
+
+ MyStruct
+
+ value += rhs.value;
+
+
+ void
+
+ int
+
+ value *= scale;
+
+
+ MyStruct*
+
+ MyStruct*
+
+ value /= other->value;
+ return other;
+
+
+
+
diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/CXXMethodDeclarationTest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/CXXMethodDeclarationTest.cs
index c737ac4e..194c916a 100644
--- a/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/CXXMethodDeclarationTest.cs
+++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/Baseline/CXXMethodDeclarationTest.cs
@@ -295,6 +295,32 @@ MyStruct MyFunction2(MyStruct lhs, MyStruct rhs)
{
return lhs - rhs;
}
+");
+
+ [Test]
+ public Task CompoundAssignmentOperatorTest()
+ => ValidateAsync(nameof(CompoundAssignmentOperatorTest), @"struct MyStruct
+{
+ int value;
+
+ MyStruct& operator+=(MyStruct rhs)
+ {
+ value += rhs.value;
+ return *this;
+ }
+
+ MyStruct& operator*=(int scale)
+ {
+ value *= scale;
+ return *this;
+ }
+
+ MyStruct& operator/=(MyStruct& other)
+ {
+ value /= other.value;
+ return other;
+ }
+};
");
[Test]
diff --git a/tests/ClangSharp.PInvokeGenerator.UnitTests/CTest.cs b/tests/ClangSharp.PInvokeGenerator.UnitTests/CTest.cs
index c4ce7c6a..22efd4b6 100644
--- a/tests/ClangSharp.PInvokeGenerator.UnitTests/CTest.cs
+++ b/tests/ClangSharp.PInvokeGenerator.UnitTests/CTest.cs
@@ -120,6 +120,27 @@ bool FromInteger(int a)
return ValidateGeneratedCSharpLatestHostBaselineAsync(inputContents, commandLineArgs: DefaultCClangCommandLineArgs, language: "c", languageStandard: DefaultCStandard);
}
+ [Test]
+ public Task CompoundLiteralExprTest()
+ {
+ // C compound literals `(T){ ... }` project as the equivalent C# object initializer
+ // (see https://github.com/dotnet/ClangSharp/issues/819).
+ var inputContents = @"struct b3Vec3
+{
+ float x;
+ float y;
+ float z;
+};
+
+struct b3Vec3 b3Negate(struct b3Vec3 a)
+{
+ return (struct b3Vec3){ -a.x, -a.y, -a.z };
+}
+";
+
+ return ValidateGeneratedCSharpLatestHostBaselineAsync(inputContents, commandLineArgs: DefaultCClangCommandLineArgs, language: "c", languageStandard: DefaultCStandard);
+ }
+
[Test]
public Task EnumTest()
{