Skip to content

Support unsafe evolution in ComInterface, ComClass, JSImport, JSExport and VtableIndexStub generators - #131701

Draft
EgorBo wants to merge 2 commits into
mainfrom
egorbo/unsafe-v2-generators
Draft

Support unsafe evolution in ComInterface, ComClass, JSImport, JSExport and VtableIndexStub generators#131701
EgorBo wants to merge 2 commits into
mainfrom
egorbo/unsafe-v2-generators

Conversation

@EgorBo

@EgorBo EgorBo commented Aug 1, 2026

Copy link
Copy Markdown
Member

Tracking issue: #131451 (section 6, "Source generator output")

This PR makes the remaining interop source generators emit code that compiles under the new unsafe-v2 rules (unsafe evolution), following #131245 which did the same for LibraryImportGenerator. Nothing changes under unsafe-v1.

[GeneratedComInterface], [GeneratedComClass], [JSImport] and [JSExport] are public API shipping since .NET 7/8, so this is real user impact: today their generated output does not compile once the switch is flipped.

Background

Every generated body relied on an unsafe modifier stamped onto the generated type for its unsafe context. Under the updated rules a type modifier opens no context for the members inside it, so those bodies stop compiling as soon as they dereference a pointer, take an address, or call a caller-unsafe API.

The fix is to give each body a context of its own:

static InterfaceImplementation()
{
+   unsafe
+   {
        ComWrappers.GetIUnknownImpl(out *(nint*)&(...)->QueryInterface_0, ...);
+   }
}

The type-level modifier also has to go, but only under the updated rules: there it has no effect and warns with CS9377, which is not suppressed in generated files and is on at the SDK's default warning level. Left in, it would reach every user as a warning from code they cannot edit, and break any build using TreatWarningsAsErrors. Under the legacy rules it is load-bearing, so it is emitted conditionally.

Changes in this PR

  1. [codegen] ComInterfaceGenerator: the static constructor, the managed-to-unmanaged stubs, the property/indexer accessor stubs and the ABI_* stubs all wrap their bodies. InterfaceInformation.ManagedVirtualMethodTable becomes a block-bodied getter so its Unsafe.AsPointer call has somewhere to put the context. ABI_* also gains an unsafe modifier, matching how Support unsafe evolution in LibraryImportGenerator #131245 keeps the inner __PInvoke caller-unsafe.

  2. [codegen] ComInterfaceGenerator shadowing members: a shadow that forwards to a caller-unsafe base member is now emitted unsafe with a block body, instead of as a plain expression-bodied forwarder. Without this the derived interface hits CS9366 (the implementation is unsafe because it copies the user's modifier, while the shadow it implements is not) and CS9362 (the forwarding call itself). An expression body cannot host the context, because unsafe(...) is not one of the forms a statement may consist of.

  3. [codegen] ComClassGenerator: GetComInterfaceEntries wraps its body; it reads details.ManagedVirtualMethodTable, a void** property.

  4. [codegen] JSImportGenerator / JSExportGenerator: bodies wrapped, and the generated __GeneratedInitializer class loses its unsafe modifier, since none of its members names a pointer type.

  5. [codegen] VtableIndexStubGenerator: PopulateUnmanagedVirtualMethodTable wraps its body.

  6. [cleanup] SyntaxExtensions.WrapInUnsafeBlock replaces the Block(UnsafeStatement(body)) pattern, which this PR would otherwise have spelled out in five more places. The two pre-existing occurrences in LibraryImportGenerator and DownlevelLibraryImportGenerator are moved over to it as well.

  7. [tests] UnsafeCodeGeneration.cs in both generator test projects runs each generator with updated-memory-safety-rules enabled and asserts the output has no errors. The JS one also asserts no CS9377, which is otherwise suppressed in // <auto-generated/> files.

Why the type-level unsafe modifier is conditional rather than gone

Under the legacy rules, naming a pointer type in a member signature requires an enclosing unsafe context, and a generated stub copies its modifiers from the user's declaration. A user is free to write

[GeneratedComInterface]
unsafe partial interface IFoo          // 'unsafe' on the type
{
    void Method(void* pBuffer);        // ... not on the member
}

in which case the generated stub has no modifier of its own and needs the type to supply the context. Removing the type-level modifier unconditionally produces CS0214 on exactly this shape.

It cannot be fixed by marking the generated stub unsafe instead: the stub explicitly implements the user's interface member, that member is not caller-unsafe under the updated rules, and an unsafe member cannot implement a safe one (CS9366).

So the modifier is emitted only when the updated rules are off. Everywhere the generator controls the members itself - the vtable struct's fields, ManagedVirtualMethodTable, GetComInterfaceEntries, ABI_* - the modifier moved onto the member, which is meaningful under both sets of rules and needs no condition. That leaves InterfaceImplementation and the user's own containing types as the only conditional cases.

The flag travels on EnvironmentFlags, which already reaches every generator, sourced from ParseOptionsProvider.

A note on testing this

The generator test harness compiles with LanguageVersion.Preview, and preview already relaxes the pointer-declaration rule even without the feature flag. A generator change that breaks unsafe-v1 therefore still passes the entire existing suite.

CS9377 has the same problem for a different reason: it sits above the test framework's default warning level, so an assertion on it silently never fires. The new tests raise the warning level to make it observable, which is what ILLink.RoslynAnalyzer.Tests already does for the same diagnostic.

Both gaps were hit while writing this PR. On top of the in-harness tests, the generated output was verified out-of-band by compiling it in all three configurations that matter:

errors CS9377
unsafe-v2 (preview + feature) none 0
unsafe-v1, LangVersion=latest none 0
unsafe-v1, LangVersion=13.0 none 0

Known limitations / follow ups

  • The open design question from the tracking issue is untouched: whether [GeneratedComInterface] methods should require an explicit safe/unsafe contract the way [LibraryImport] methods now do (SYSLIB1064). That is an API decision rather than a codegen one.
  • ComClassGenerator and ComInterfaceGenerator write their output as strings rather than syntax nodes, so the added nesting is hand-indented.

Note

This description and the changes in this PR were generated with GitHub Copilot.

Every generated body relied on an 'unsafe' modifier on the generated type for
its unsafe context. Under the updated memory safety rules a type modifier opens
no context for the members inside it, so those bodies stop compiling as soon as
they dereference a pointer, take an address, or call a caller-unsafe API.

Give each body a context of its own instead. A shadowing member that forwards to
a caller-unsafe base member additionally has to declare itself unsafe and take a
block body, since its implementation is unsafe and an expression body has nowhere
to put the context.

The type-level modifier stays. Under the legacy rules it is what makes a pointer
legal to name in a stub that copies its modifiers from a user declaration
carrying 'unsafe' on the type rather than on the member, and the stub cannot take
the modifier itself without failing to implement the member it implements.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 06c64005-a797-4517-9227-d1706eb5bca5
Copilot AI review requested due to automatic review settings August 1, 2026 17:41
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/interop-contrib
See info in area-owners.md if you want to be subscribed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates several interop source generators so their emitted code compiles when the C# “updated memory safety rules” (unsafe evolution / unsafe-v2) are enabled, by ensuring generated member bodies establish their own unsafe { } context instead of relying on an unsafe modifier on the containing type. It also adds/adjusts unit tests to compile generator output with the feature flag enabled and assert no compilation errors.

Changes:

  • Add a shared SyntaxExtensions.WrapInUnsafeBlock helper and use it in multiple generators to wrap generated bodies in unsafe { }.
  • Update COM and JS interop generator output to open explicit unsafe contexts for pointer/address-taking/caller-unsafe operations (including shadowing member forwarders).
  • Add new unit tests (COM + JS) that compile generated output with updated-memory-safety-rules enabled and assert the compilation is clean.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/UnsafeCodeGeneration.cs New tests compiling ComInterface/ComClass/VtableIndexStub generator output under updated rules.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/ComInterfaceGeneratorOutputShape.cs Narrow accessor-attribute assertions to the intended generated type to avoid unrelated accessors.
src/libraries/System.Runtime.InteropServices/tests/ComInterfaceGenerator.Unit.Tests/ComInterfaceGenerator.Unit.Tests.csproj Link in shared MemorySafetyRules feature-flag constant for updated-rules opt-in.
src/libraries/System.Runtime.InteropServices/gen/Microsoft.Interop.SourceGeneration/SyntaxExtensions.cs Add WrapInUnsafeBlock helper for consistent unsafe-block wrapping.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.cs Switch to WrapInUnsafeBlock for generated stub bodies.
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.cs Same WrapInUnsafeBlock adoption for downlevel generator.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/VtableIndexStubGenerator.cs Wrap vtable population body in an explicit unsafe { } block (string-emitted code).
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/VirtualMethodPointerStubGenerator.cs Wrap generated managed/unmanaged stub bodies and accessor bodies in unsafe { }; mark ABI stubs unsafe.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/ComInterfaceGenerator.cs Add explicit unsafe contexts for generated static ctor and pointer-returning accessor; adjust shadow emission to host unsafe contexts.
src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/ComClassGenerator.cs Wrap GetComInterfaceEntries body in an explicit unsafe { } block (string-emitted code).
src/libraries/System.Runtime.InteropServices.JavaScript/tests/JSImportGenerator.UnitTest/UnsafeCodeGeneration.cs New test compiling JSImport/JSExport generator output under updated rules and asserting no errors.
src/libraries/System.Runtime.InteropServices.JavaScript/tests/JSImportGenerator.UnitTest/JSImportGenerator.Unit.Tests.csproj Include new JS unsafe-evolution compilation test.
src/libraries/System.Runtime.InteropServices.JavaScript/tests/JSImportGenerator.UnitTest/Compiles.cs Update expected generated output to match new unsafe-block wrapping and initializer class modifier change.
src/libraries/System.Runtime.InteropServices.JavaScript/gen/JSImportGenerator/JSImportGenerator.cs Wrap generated JSImport method bodies in unsafe { } using the shared helper.
src/libraries/System.Runtime.InteropServices.JavaScript/gen/JSImportGenerator/JSExportGenerator.cs Wrap wrapper body in unsafe { }; remove unnecessary unsafe modifier from generated initializer class.

The generated types carried an 'unsafe' modifier unconditionally. Under the
updated memory safety rules that modifier has no effect and warns (CS9377),
which is not suppressed in generated files and is on at the SDK's default
warning level, so it would reach every user as a warning from code they cannot
edit.

Emit it only when the updated rules are off, and where the generator controls
the members put the modifier on them instead, which is meaningful under both
sets of rules. The one type that still needs it under the legacy rules is
InterfaceImplementation, whose stubs explicitly implement user interface
members and so cannot carry a modifier of their own.

The flag travels on EnvironmentFlags, which already reaches the generators.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 06c64005-a797-4517-9227-d1706eb5bca5
Copilot AI review requested due to automatic review settings August 1, 2026 19:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/libraries/System.Runtime.InteropServices/gen/ComInterfaceGenerator/ComClassInfo.cs:73

  • ComClassInfo.Equals now includes UseUpdatedMemorySafetyRules, but GetHashCode() does not. While this doesn't break correctness, it increases hash collisions and is inconsistent with the equality definition (and can reduce incremental generator caching effectiveness). Include UseUpdatedMemorySafetyRules in the hash computation.
        public bool Equals(ComClassInfo? other)
        {
            return other is not null
                && ClassName == other.ClassName
                && ContainingSyntaxContext.Equals(other.ContainingSyntaxContext)
                && UseUpdatedMemorySafetyRules == other.UseUpdatedMemorySafetyRules
                && ImplementedInterfacesNames.SequenceEqual(other.ImplementedInterfacesNames);
        }

        public override bool Equals(object obj)
        {
            return Equals(obj as ComClassInfo);
        }

        public override int GetHashCode()
        {
            return HashCode.Combine(ClassName, ContainingSyntaxContext, ImplementedInterfacesNames);
        }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants