Skip to content

[Breaking change]: Native AOT now throws InvalidProgramException for [UnmanagedCallersOnly] methods with non-blittable parameters when runtime marshalling is enabled #54647

Description

@MichalStrehovsky

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

  • Behavioral change: Existing binaries might behave differently at run time.

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

Metadata

Metadata

Assignees

Labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions