Skip to content
32 changes: 26 additions & 6 deletions sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.VisitDecl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2587,7 +2587,7 @@ void VisitBitfieldDecl(FieldDecl fieldDecl, BitfieldDesc[] bitfieldDescs, Record

case CXType_ULongLong:
{
if (typeNameBacking.Equals("nuint", StringComparison.Ordinal))
if (typeNameBacking.Equals("nuint", StringComparison.Ordinal)) // TODO: Shouldn't this also check UIntPtr?
{
goto case CXType_UInt;
}
Expand Down Expand Up @@ -2620,7 +2620,7 @@ void VisitBitfieldDecl(FieldDecl fieldDecl, BitfieldDesc[] bitfieldDescs, Record
{
isTypeBackingSigned = true;

if (typeNameBacking.Equals("nint", StringComparison.Ordinal))
if (typeNameBacking.Equals("nint", StringComparison.Ordinal)) // TODO: Shouldn't this also check IntPtr?
{
goto case CXType_Int;
}
Expand Down Expand Up @@ -2686,7 +2686,7 @@ void VisitBitfieldDecl(FieldDecl fieldDecl, BitfieldDesc[] bitfieldDescs, Record

case CXType_ULongLong:
{
if (typeNameBacking.Equals("nuint", StringComparison.Ordinal))
if (typeNameBacking.Equals("nuint", StringComparison.Ordinal)) // TODO: Shouldn't this also check UIntPtr?
{
goto case CXType_UInt;
}
Expand Down Expand Up @@ -2719,7 +2719,7 @@ void VisitBitfieldDecl(FieldDecl fieldDecl, BitfieldDesc[] bitfieldDescs, Record
{
isTypeSigned = true;

if (typeNameBacking.Equals("nint", StringComparison.Ordinal))
if (typeNameBacking.Equals("nint", StringComparison.Ordinal)) // TODO: Shouldn't this also check IntPtr?
{
goto case CXType_Int;
}
Expand Down Expand Up @@ -2773,7 +2773,7 @@ void VisitBitfieldDecl(FieldDecl fieldDecl, BitfieldDesc[] bitfieldDescs, Record

// Signed types are sign extended when shifted
var isUnsignedToSigned = !isTypeBackingSigned && isTypeSigned;

// Check if type is directly shiftable/maskable
// Remapped types are not guaranteed to be shiftable or maskable
// Enums are maskable, but not shiftable
Expand Down Expand Up @@ -3981,7 +3981,27 @@ private bool IsConstant(string targetTypeName, Expr initExpr)

case CX_StmtClass_IntegerLiteral:
{
return true;
var notConstant = false;

// Constant expressions for native integers must be in range: https://github.com/dotnet/csharplang/blob/main/proposals/csharp-9.0/native-integers.md

// TODO: C#'s compiler only checks for the range when outputting as a nuint/nint. See constant folding in the native-integers proposal.
// These compile fine as const fields:
// Eg: nuint = (nuint)(4294967296 - 10) // The integer literal itself is constant
// Eg: nuint = unchecked(4294967295 + 10)
// Eg: nint = unchecked((nint)4294967295)
// TODO: This means for proper analysis we need to evaluate the value of the expression, which seems out of scope

// TODO: This condition leads to more change in output than ideal due to the behavior described above.
// TODO: This causes existing tests to fail. Eg: ClangSharp.UnitTests.VarDeclarationTest.UncheckedConversionMacroTest. "const nint MyMacro1 = unchecked((nint)(0x80000000))" incorrectly becomes "static readonly nint MyMacro1 = unchecked((nint)(0x80000000))"
// notConstant |= targetTypeName is "nuint" or "UIntPtr" && initExpr is IntegerLiteral { UnsignedValue: > uint.MaxValue };
// notConstant |= targetTypeName is "nint" or "IntPtr" && initExpr is IntegerLiteral { Value: < int.MinValue or > int.MaxValue };

// TODO: This condition leads to less change in output than ideal, but avoids incorrectly failing existing tests. This might be the best we can do without proper expression value evaluation.
notConstant |= targetTypeName is "nuint" or "UIntPtr" && initExpr is IntegerLiteral { UnsignedValue: > uint.MaxValue };
notConstant |= targetTypeName is "nint" or "IntPtr" && initExpr is IntegerLiteral { Value: < int.MinValue or > uint.MaxValue }; // Note the uint.MaxValue here

return !notConstant;
}

case CX_StmtClass_LambdaExpr:
Expand Down
14 changes: 14 additions & 0 deletions sources/ClangSharp.PInvokeGenerator/PInvokeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5847,6 +5847,13 @@ private bool IsUnchecked(string targetTypeName, Stmt stmt)
{
var integerLiteral = (IntegerLiteral)stmt;
var signedValue = integerLiteral.Value;

if ((targetTypeName is "nuint" or "UIntPtr" && integerLiteral is { UnsignedValue: > uint.MaxValue })
|| (targetTypeName is "nint" or "IntPtr" && integerLiteral is { Value: < int.MinValue or > int.MaxValue }))
{
return true;
}

return IsUnchecked(targetTypeName, signedValue, integerLiteral.IsNegative, isHex: integerLiteral.ValueString.StartsWith("0x", StringComparison.Ordinal));
}

Expand Down Expand Up @@ -6780,6 +6787,13 @@ private void UncheckStmt(string targetTypeName, Stmt stmt)
}
}

if (IsPrevContextDecl<VarDecl>(out _, out _)
&& ((targetTypeName is "nuint" or "UIntPtr" && stmt.Handle.Evaluate.AsUnsigned > uint.MaxValue)
|| (targetTypeName is "nint" or "IntPtr" && stmt.Handle.Evaluate.AsLongLong is < int.MinValue or > uint.MaxValue)))
{
needsCast = true;
}

if (needsCast)
{
_outputBuilder.BeginInnerValue();
Expand Down
81 changes: 81 additions & 0 deletions tests/ClangSharp.PInvokeGenerator.UnitTests/CTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1002,4 +1002,85 @@ readonly get
{ "Flags", "Flags" }
});
}

[Test]
[Platform("unix")] // This test has slight platform-specific differences (on Windows, __LONG_MAX__ is 32-bit)
public Task CLongDefinesTestUnix()
{
// C longs differ based on platform
// These values are taken from the Linux headers when using Clang
var inputContents = @"
// stdint.h
#define SIZE_MAX (18446744073709551615UL)

// cl_ext.h from OpenCL
#define CL_IMPORT_MEMORY_WHOLE_ALLOCATION_ARM SIZE_MAX

// limits.h
#define LONG_MAX __LONG_MAX__
#define ULONG_MAX (__LONG_MAX__ *2UL+1UL)
";

// We use "static readonly" instead of "const" because nint/nuint differ on 32/64-bit platforms
var expectedOutputContents = @"namespace ClangSharp.Test
{
public static partial class Methods
{
[NativeTypeName(""#define SIZE_MAX (18446744073709551615UL)"")]
public static readonly nuint SIZE_MAX = unchecked((nuint)(18446744073709551615U));

[NativeTypeName(""#define CL_IMPORT_MEMORY_WHOLE_ALLOCATION_ARM SIZE_MAX"")]
public static readonly nuint CL_IMPORT_MEMORY_WHOLE_ALLOCATION_ARM = unchecked((nuint)(18446744073709551615U));

[NativeTypeName(""#define LONG_MAX __LONG_MAX__"")]
public static readonly nint LONG_MAX = unchecked((nint)(9223372036854775807));

[NativeTypeName(""#define ULONG_MAX (__LONG_MAX__ *2UL+1UL)"")]
public static readonly nuint ULONG_MAX = unchecked((nuint)(9223372036854775807 * 2U + 1U));
}
}
";

return ValidateGeneratedCSharpLatestUnixBindingsAsync(inputContents, expectedOutputContents, commandLineArgs: DefaultCClangCommandLineArgs, language: "c", languageStandard: DefaultCStandard);
}

[Test]
[Platform("win")] // This test has slight platform-specific differences
public Task CLongDefinesTestWindows()
{
// C longs differ based on platform
// These values are taken from the Windows headers when using MSVC
var inputContents = @"
// limits.h
#define SIZE_MAX 0xffffffffffffffffui64

// cl_ext.h from OpenCL
#define CL_IMPORT_MEMORY_WHOLE_ALLOCATION_ARM SIZE_MAX

// limits.h
#define LONG_MAX 2147483647L
#define ULONG_MAX 0xffffffffUL
";

var expectedOutputContents = @"namespace ClangSharp.Test
{
public static partial class Methods
{
[NativeTypeName(""#define SIZE_MAX 0xffffffffffffffffui64"")]
public const ulong SIZE_MAX = 0xffffffffffffffffUL;

[NativeTypeName(""#define CL_IMPORT_MEMORY_WHOLE_ALLOCATION_ARM SIZE_MAX"")]
public const ulong CL_IMPORT_MEMORY_WHOLE_ALLOCATION_ARM = 0xffffffffffffffffUL;

[NativeTypeName(""#define LONG_MAX 2147483647L"")]
public const int LONG_MAX = 2147483647;

[NativeTypeName(""#define ULONG_MAX 0xffffffffUL"")]
public const uint ULONG_MAX = 0xffffffffU;
}
}
";

return ValidateGeneratedCSharpLatestWindowsBindingsAsync(inputContents, expectedOutputContents, commandLineArgs: DefaultCClangCommandLineArgs, language: "c", languageStandard: DefaultCStandard);
}
}
Loading