Skip to content

Support unsafe evolution in LibraryImportGenerator - #131245

Open
EgorBo wants to merge 1 commit into
dotnet:mainfrom
EgorBo:egorbo/libraryimport-unsafe-evolution
Open

Support unsafe evolution in LibraryImportGenerator#131245
EgorBo wants to merge 1 commit into
dotnet:mainfrom
EgorBo:egorbo/libraryimport-unsafe-evolution

Conversation

@EgorBo

@EgorBo EgorBo commented Jul 23, 2026

Copy link
Copy Markdown
Member

This PR makes [LibraryImport] participate in the new unsafe-v2 rules (unsafe evolution), plus tooling to migrate existing code. Nothing changes under unsafe-v1:

  • An analyzer + code-fixer to prepare codebase to unsafe-v2 by adding unsafe on all [LibraryImport] if they had no safety keyword on them (migration).
  • Changed LibraryImportGenerator (to be precise: LibraryImportDiagnosticsAnalyzer and  DownlevelLibraryImportDiagnosticsAnalyzer) to explicitly require safe or unsafe under the new rules with human-readable message.

Background

The compiler requires an explicit safe/unsafe modifier on extern members (CS9389). But a [LibraryImport] method is only implemented by an extern forwarder when its signature needs no marshalling - otherwise the generator emits a managed wrapper around a private extern local function. So today the requirement fires for some P/Invokes and not others, based on marshalling alone. The speclet calls this out (safe on non-extern members):

Scenarios that need to require an explicit modifier when the language does not, such as a LibraryImport that generates a non-extern wrapper, will need an analyzer to enforce the presence of safe or unsafe.

Changes in this PR

  1. [analyzer] SYSLIB1064 (see 'Diagnostics IDs' below) requires an explicit safe/unsafe modifier on every method with LibraryImportAttribute when the updated rules are enabled - for every shape, so adding a string parameter never silently changes a P/Invoke's safety obligations. Reported by both LibraryImportDiagnosticsAnalyzer and its downlevel counterpart.

  2. [tests] The private extern stays caller-unsafe. No codegen change here: main already emits the inner __PInvoke local function as static extern unsafe and wraps stub bodies in an explicit unsafe block. An earlier revision of this PR mirrored the user-facing modifier onto that local function, which @jkotas pointed out violates the model - the raw P/Invoke taking char* is obviously unsafe, while the wrapper is what discharges the obligation - so it was dropped and replaced with tests that lock the behavior in. The user's modifier is still mirrored onto the generated wrapper, so both halves of the partial agree.

  3. [analyzer] IL5007 reports methods with LibraryImportAttribute that declare no safety contract. Unlike SYSLIB1064 it fires regardless of the opt-in, so a code base can be annotated before the switch is flipped.

  4. [fixer] AddUnsafeToLibraryImportCodeFixProvider fixes IL5007 by marking the method unsafe by default; developers can replace it with safe after auditing the boundary. This is the [LibraryImport] counterpart of AddUnsafeToExternCodeFixProvider from Add unsafe modifier migration code fixer #131002 and, like the rest of that tooling, is not shipping (#if DEBUG) and off by default.

Since CSharpCompilationOptions.MemorySafetyRules is still internal (dotnet/roslyn#82546), the opt-in is detected via the updated-memory-safety-rules feature flag - the same fallback Roslyn's own SourceModuleSymbol.UseUpdatedMemorySafetyRules uses.

Diagnostics IDs

Just for reference

  • CS9389 [Roslyn] - An extern member must be explicitly marked unsafe or safe.
  • CS9388 [Roslyn] - The safe modifier is only valid on non-unsafe extern members or field-like members of explicit or extended-layout types.
  • CS0764 [Roslyn] - Both partial member declarations must be unsafe, or neither may be unsafe.
  • CS9390 [Roslyn] - Both partial member declarations must be marked safe, or neither may be marked safe.
  • SYSLIB1064 [This PR] - A method with LibraryImportAttribute must be marked safe or unsafe under the updated rules.
  • IL5007 [This PR] - A method with LibraryImportAttribute has no explicit safety contract (migration only, needed for the code-fixer).

Alternative design

Instead of a new analyzer, the generator could emit its own part as unsafe and let the language enforce the rest: the user gets CS0764 until they write unsafe too, or they write safe and we regenerate to match. Tempting - no new diagnostic ID, and no CS9389+SYSLIB1064 doubling up in the forwarder shape. It was rejected because:

  • The error lands in generated code. SourceOrdinaryMethodSymbol.PartialMethodChecks reports both CS0764 and CS9390 at implementation.GetFirstLocation(), i.e. inside LibraryImports.g.cs. No code fix can be offered there, and the message never mentions P/Invoke. CS9389 by contrast does land on the user's declaration, so the two shapes would report in different files with different wording.
  • It only helps the wrapper shape - the forwarder shape is already covered by CS9389.
  • Generator output would start depending on the opt-in, which has to be threaded through the incremental pipeline or every existing unsafe-v1 P/Invoke gets CS0764.
  • CS0764 is suppressed when AllowUnsafeBlocks is off (&& definition.CompilationAllowsUnsafe()), so enforcement would silently disappear in that configuration.

The speclet asks the same question ("should the language provide a narrower rule for partial members implemented by source generators?") and the working group answered: use an analyzer.

Known limitations / follow ups

  • Roslyn does not allow safe on non-extern members yet (CS9388); Unsafe evolution: relax safe modifier placement restrictions roslyn#84602 lifts this and is motivated by exactly this scenario. Until then safe can only be spelled on P/Invokes whose generated implementation is a forwarder, so tests only exercise safe in that shape.
  • ConvertToLibraryImportFixer preserves unsafe when rewriting a [DllImport] (new test) but drops safe, since SyntaxGenerator does not model the modifier and carrying it over today would produce code hitting CS9388.

Copilot AI review requested due to automatic review settings July 23, 2026 00:02
@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.

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

Updates LibraryImportGenerator/Analyzers to participate in the “updated memory safety rules” evolution by (1) requiring [LibraryImport] methods to opt in with an explicit safe/unsafe modifier when the feature is enabled and (2) mirroring the selected modifier onto generated extern signatures (including the wrapper-path inner local DllImport).

Changes:

  • Add a new diagnostic (SYSLIB1050) to fail [LibraryImport] declarations missing an explicit safe/unsafe modifier when updated memory-safety rules are enabled.
  • Flow the user’s safety modifier through to generated wrapper local extern signatures (LibraryImportGenerator + Downlevel variant).
  • Add shared helpers + update unit tests and localized resource strings.

Reviewed changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/UnsafeCodeGeneration.cs Expands shape tests to cover safe/unsafe mirroring and missing-modifier diagnostics under the feature flag.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.csproj Links in the new shared extensions helper.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.cs Passes the extracted safety modifier into wrapper-path inner DllImport local function generation.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/GeneratorDiagnostics.cs Adds the new diagnostic descriptor for “missing safety modifier”.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/Analyzers/LibraryImportDiagnosticsAnalyzer.cs Includes + emits the new missing-safety-modifier diagnostic when the feature is enabled.
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/GeneratorDiagnostics.cs Adds downlevel counterpart diagnostic descriptor.
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.csproj Links in the new shared extensions helper.
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.cs Mirrors safety modifier onto wrapper-path inner DllImport local function (downlevel).
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportDiagnosticsAnalyzer.cs Includes + emits missing-safety-modifier diagnostic (downlevel).
src/libraries/System.Runtime.InteropServices/gen/Common/LibraryImportGeneratorExtensions.cs Adds shared helpers to detect safety modifiers and the updated-rules feature flag.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/Strings.resx Adds strings for the new diagnostic message/description.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.de.xlf Adds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.es.xlf Adds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.fr.xlf Adds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.it.xlf Adds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ja.xlf Adds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ko.xlf Adds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pl.xlf Adds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pt-BR.xlf Adds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ru.xlf Adds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.tr.xlf Adds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hans.xlf Adds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hant.xlf Adds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.cs.xlf Adds localization entries for the new diagnostic.

@EgorBo
EgorBo marked this pull request as draft July 23, 2026 00:11
Copilot AI review requested due to automatic review settings July 23, 2026 00:27

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 24 out of 24 changed files in this pull request and generated no new comments.

@jkotas

jkotas commented Jul 23, 2026

Copy link
Copy Markdown
Member

Mirror the modifier onto generated extern signatures for both generator variants.

This does not sound right.

If I define a PInvoke like:

[LibraryImport(...)]
static partial safe void PrintString(string s);

The actual internal PInvoke is going to be static extern void PrintString(char* s). This method should not be marked as safe. It would be violating the model.

@EgorBo

EgorBo commented Jul 23, 2026

Copy link
Copy Markdown
Member Author

Mirror the modifier onto generated extern signatures for both generator variants.

This does not sound right.

If I define a PInvoke like:

[LibraryImport(...)]
static partial safe void PrintString(string s);

The actual internal PInvoke is going to be static extern void PrintString(char* s). This method should not be marked as safe. It would be violating the model.

@jkotas do you mean that in this case the private extern hidden in the generator should be caller-unsafe or that the entire signature void PrintString(string s); should never be marked as safe? I had this in mind, I just though that if user is sure that the pinvoke is always safe then even the extern that user won't see will be safe - it can be easily changed to be always unsafe.

Copilot AI review requested due to automatic review settings July 23, 2026 08:11

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 24 out of 24 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/UnsafeCodeGeneration.cs:149

  • Same concern here: including a downlevel=true test case likely routes through TestTargetFramework.Standard2_0 ref packs, which are commonly handled as outer-loop due to restore/network requirements. Consider moving the downlevel coverage to a dedicated [OuterLoop] test and keeping this theory non-downlevel for regular runs.
        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public Task UpdatedMemorySafetyRulesRequireExplicitSafetyModifier(bool downlevel)

@jkotas

jkotas commented Jul 23, 2026

Copy link
Copy Markdown
Member

The outer signature should be safe. The private extern hidden in the generated code should be caller-unsafe. Consider my example - the raw PInvoke that takes ´char*´ is obviously unsafe.

Copilot AI review requested due to automatic review settings July 23, 2026 09:25

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 22 out of 22 changed files in this pull request and generated no new comments.

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 24 out of 24 changed files in this pull request and generated no new comments.

@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.

Under the updated memory safety rules the compiler requires an explicit
'safe' or 'unsafe' modifier on every 'extern' member, but a method with
'LibraryImportAttribute' is only implemented by an 'extern' forwarder when
its signature needs no marshalling. Require the modifier for every shape so
the contract does not depend on that generator implementation detail, and
add the migration analyzer and code fixer that default unannotated methods
to 'unsafe'.

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 July 27, 2026 23:05
@EgorBo
EgorBo force-pushed the egorbo/libraryimport-unsafe-evolution branch from 0a48d9a to 8600571 Compare July 27, 2026 23:05
@dotnet-policy-service dotnet-policy-service Bot added linkable-framework Issues associated with delivering a linker friendly framework labels Jul 27, 2026

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 33 out of 33 changed files in this pull request and generated no new comments.

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

Labels

area-System.Runtime.InteropServices linkable-framework Issues associated with delivering a linker friendly framework

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants