Description
[UnmanagedCallersOnly] attribute contract requires all parameter and return types to be blittable. CoreCLR and ReadyToRun already enforced this restriction; Native AOT was missing the validation. This change brings Native AOT into alignment.
[UnmanagedCallersOnly] does not perform any marshaling, which is why this requirement was enforced with CoreCLR and ReadyToRun. Previously, Native AOT silently accepted these signatures and generated code that passed values through without any conversion, even though the signature might imply marshalling.
PR: dotnet/runtime#130160
Version
.NET 11 Preview 7
Previous behavior
In Native AOT, an [UnmanagedCallersOnly] method with non-blittable parameters or return type compiled and ran without error:
using System.Runtime.InteropServices;
public static class Exports
{
[UnmanagedCallersOnly(EntryPoint = "ReturnsBool")]
public static bool ReturnsBool() // 'bool' requires marshalling — previously silently accepted by NativeAOT
{
return true;
}
[UnmanagedCallersOnly(EntryPoint = "TakesChar")]
public static void TakesChar(char c) // 'char' requires marshalling — previously silently accepted by NativeAOT
{
Console.WriteLine(c);
}
}
New behavior
In Native AOT, the same code now throws InvalidProgramException at run time indicating non-blittable types were used:
using System.Runtime.InteropServices;
public static class Exports
{
// Now throws InvalidProgramException — 'bool' is not blittable when runtime marshalling is enabled
[UnmanagedCallersOnly(EntryPoint = "ReturnsBool")]
public static bool ReturnsBool()
{
return true;
}
}
This behavior is now consistent with CoreCLR and ReadyToRun, which already rejected such signatures.
Note this exception is not catchable because exceptions from UnmanagedCallersOnly methods result in a fail fast.
Type of breaking change
Reason for change
The [UnmanagedCallersOnly] attribute requires all parameter and return types to be blittable—that is, they must have the same binary representation in managed and native code. When runtime marshalling is enabled (the default for assemblies without [assembly: DisableRuntimeMarshalling]), types such as bool and char are not blittable: bool is marshalled as a 4-byte BOOL on Windows (or with platform-specific semantics), and char is marshalled as a 2-byte Unicode character. Passing these types through an [UnmanagedCallersOnly] boundary without marshalling can result in silent data corruption.
CoreCLR and ReadyToRun already enforced this restriction, but Native AOT was missing the validation
Recommended action
Option 1: If you're using types that become blittable with runtime marshalling disabled (char, bool) add [assembly: DisableRuntimeMarshalling] to the assembly containing the [UnmanagedCallersOnly] method. This disables runtime marshalling for the entire assembly, treating all interop types as blittable. Values are passed through without conversion, so ensure that the native caller and managed callee agree on the exact binary representation of each type.
// Add to AssemblyInfo.cs or at the top of any source file in the assembly
[assembly: System.Runtime.CompilerServices.DisableRuntimeMarshalling]
using System.Runtime.InteropServices;
public static class Exports
{
// OK — DisableRuntimeMarshalling makes 'bool' blittable (1-byte value, no marshalling)
[UnmanagedCallersOnly(EntryPoint = "ReturnsBool")]
public static bool ReturnsBool()
{
return true;
}
}
Option 2: Replace non-blittable types with blittable equivalents.
Feature area
Interop
Affected APIs
System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute
Description
[UnmanagedCallersOnly]attribute contract requires all parameter and return types to be blittable. CoreCLR and ReadyToRun already enforced this restriction; Native AOT was missing the validation. This change brings Native AOT into alignment.[UnmanagedCallersOnly]does not perform any marshaling, which is why this requirement was enforced with CoreCLR and ReadyToRun. Previously, Native AOT silently accepted these signatures and generated code that passed values through without any conversion, even though the signature might imply marshalling.PR: dotnet/runtime#130160
Version
.NET 11 Preview 7
Previous behavior
In Native AOT, an
[UnmanagedCallersOnly]method with non-blittable parameters or return type compiled and ran without error:New behavior
In Native AOT, the same code now throws
InvalidProgramExceptionat run time indicating non-blittable types were used:This behavior is now consistent with CoreCLR and ReadyToRun, which already rejected such signatures.
Note this exception is not catchable because exceptions from UnmanagedCallersOnly methods result in a fail fast.
Type of breaking change
Reason for change
The
[UnmanagedCallersOnly]attribute requires all parameter and return types to be blittable—that is, they must have the same binary representation in managed and native code. When runtime marshalling is enabled (the default for assemblies without[assembly: DisableRuntimeMarshalling]), types such asboolandcharare not blittable:boolis marshalled as a 4-byteBOOLon Windows (or with platform-specific semantics), andcharis marshalled as a 2-byte Unicode character. Passing these types through an[UnmanagedCallersOnly]boundary without marshalling can result in silent data corruption.CoreCLR and ReadyToRun already enforced this restriction, but Native AOT was missing the validation
Recommended action
Option 1: If you're using types that become blittable with runtime marshalling disabled (
char,bool) add[assembly: DisableRuntimeMarshalling]to the assembly containing the[UnmanagedCallersOnly]method. This disables runtime marshalling for the entire assembly, treating all interop types as blittable. Values are passed through without conversion, so ensure that the native caller and managed callee agree on the exact binary representation of each type.Option 2: Replace non-blittable types with blittable equivalents.
Feature area
Interop
Affected APIs
System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute