Support unsafe evolution in LibraryImportGenerator - #131245

Merged
EgorBo merged 4 commits into
dotnet:mainfrom
EgorBo:egorbo/libraryimport-unsafe-evolution
Aug 1, 2026
Merged

Support unsafe evolution in LibraryImportGenerator#131245
EgorBo merged 4 commits into
dotnet:mainfrom
EgorBo:egorbo/libraryimport-unsafe-evolution

Conversation

@EgorBo

@EgorBoEgorBo commented Jul 23, 2026

Copy link
Copy Markdown
Member

Tracking issue: #131451

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.

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

CopilotAI 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
FileDescription
src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/UnsafeCodeGeneration.csExpands shape tests to cover safe/unsafe mirroring and missing-modifier diagnostics under the feature flag.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.csprojLinks in the new shared extensions helper.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.csPasses the extracted safety modifier into wrapper-path inner DllImport local function generation.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/GeneratorDiagnostics.csAdds the new diagnostic descriptor for “missing safety modifier”.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/Analyzers/LibraryImportDiagnosticsAnalyzer.csIncludes + emits the new missing-safety-modifier diagnostic when the feature is enabled.
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/GeneratorDiagnostics.csAdds downlevel counterpart diagnostic descriptor.
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.csprojLinks in the new shared extensions helper.
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.csMirrors safety modifier onto wrapper-path inner DllImport local function (downlevel).
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportDiagnosticsAnalyzer.csIncludes + emits missing-safety-modifier diagnostic (downlevel).
src/libraries/System.Runtime.InteropServices/gen/Common/LibraryImportGeneratorExtensions.csAdds shared helpers to detect safety modifiers and the updated-rules feature flag.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/Strings.resxAdds strings for the new diagnostic message/description.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.de.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.es.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.fr.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.it.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ja.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ko.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pl.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pt-BR.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ru.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.tr.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hans.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hant.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.cs.xlfAdds localization entries for the new diagnostic.

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

CopilotAI 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

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

Copy link
Copy Markdown
MemberAuthor

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.

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

CopilotAI 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

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.

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

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

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

@EgorBo

EgorBo commented Jul 30, 2026

Copy link
Copy Markdown
MemberAuthor

PTAL @jtschuster@jkoritzinsky cc @jjonescz@333fred

  • Changed LibraryImportGenerator (to be precise: LibraryImportDiagnosticsAnalyzer and DownlevelLibraryImportDiagnosticsAnalyzer) to explicitly require safe or unsafe under the new rules with human-readable message on the user's side. It then is copied to the generated side. If extern is not directly forwarded, it's always internally marked as unsafe.
  • 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). - we then can audit and replace with safe.

- The SYSLIB1064 description no longer explains which shape the generator
emits. The requirement matches what the language asks of 'extern' members,
which is the part that matters to the reader.
- IL5007 stands down once the assembly is on the updated rules, where
SYSLIB1064 already reports the same methods. Its tests now run under the
legacy rules, which is the situation the analyzer exists for.
- The ConvertToLibraryImport fixer no longer drops an explicit 'safe' modifier.
DeclarationModifiers cannot represent it, so a declaration rebuilt through
it silently widened the method's contract to its callers.
- The downlevel analyzer has its own tests for the diagnostic, and the tests
reuse the feature flag constant rather than repeating the string.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 06c64005-a797-4517-9227-d1706eb5bca5
CopilotAI review requested due to automatic review settings July 30, 2026 20:32

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

@AaronRobinsonMSFTAaronRobinsonMSFT added reduce-unsafe source-generator Indicates an issue with a source generator feature and removed linkable-framework Issues associated with delivering a linker friendly framework labels Jul 30, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Jul 30, 2026
@AaronRobinsonMSFT

Copy link
Copy Markdown
Member

@jkoritzinsky PTAL.

@AaronRobinsonMSFTAaronRobinsonMSFT removed the linkable-framework Issues associated with delivering a linker friendly framework label Jul 30, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Jul 30, 2026
…unsafe-evolution
# Conflicts:
#	src/tools/illink/src/ILLink.CodeFix/Resources.resx
#	src/tools/illink/src/ILLink.CodeFix/UnsafeModifierCodeFixHelpers.cs
#	src/tools/illink/src/ILLink.RoslynAnalyzer/UnsafeMigrationSyntaxHelpers.cs

@jkoritzinskyjkoritzinsky left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One comment for forward-looking cleanup/style. Other than that, LGTM

Move the helpers into extension blocks whose members are named after the
Roslyn APIs they stand in for, so 'SyntaxKind.SafeKeyword' is spelled the way
the newer Roslyn declares it. A declared member wins over an extension member,
so once the reference is updated the call sites keep compiling unchanged and
the shim is a pure deletion.
The test project already references a Roslyn that declares the keyword, where
the call sites bind to the real member and need its experimental diagnostic
suppressed. The generators reference one that does not, and keep resolving the
kind at run time.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 06c64005-a797-4517-9227-d1706eb5bca5
CopilotAI review requested due to automatic review settings August 1, 2026 07:04

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

@EgorBo
EgorBo merged commit c251738 into dotnet:mainAug 1, 2026
112 of 114 checks passed
@EgorBo
EgorBo deleted the egorbo/libraryimport-unsafe-evolution branch August 1, 2026 16:06
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-rc1 milestone Aug 2, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Sep 2, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Runtime.InteropServiceslinkable-frameworkIssues associated with delivering a linker friendly frameworkreduce-unsafesource-generatorIndicates an issue with a source generator feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@EgorBo@jkotas@AaronRobinsonMSFT@jkoritzinsky@jjonescz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Support unsafe evolution in LibraryImportGenerator - #131245

Merged
EgorBo merged 4 commits into
dotnet:mainfrom
EgorBo:egorbo/libraryimport-unsafe-evolution
Aug 1, 2026
Merged

Support unsafe evolution in LibraryImportGenerator#131245
EgorBo merged 4 commits into
dotnet:mainfrom
EgorBo:egorbo/libraryimport-unsafe-evolution

Conversation

@EgorBo

@EgorBoEgorBo commented Jul 23, 2026

Copy link
Copy Markdown
Member

Tracking issue: #131451

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.

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

CopilotAI 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
FileDescription
src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/UnsafeCodeGeneration.csExpands shape tests to cover safe/unsafe mirroring and missing-modifier diagnostics under the feature flag.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.csprojLinks in the new shared extensions helper.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.csPasses the extracted safety modifier into wrapper-path inner DllImport local function generation.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/GeneratorDiagnostics.csAdds the new diagnostic descriptor for “missing safety modifier”.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/Analyzers/LibraryImportDiagnosticsAnalyzer.csIncludes + emits the new missing-safety-modifier diagnostic when the feature is enabled.
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/GeneratorDiagnostics.csAdds downlevel counterpart diagnostic descriptor.
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.csprojLinks in the new shared extensions helper.
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.csMirrors safety modifier onto wrapper-path inner DllImport local function (downlevel).
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportDiagnosticsAnalyzer.csIncludes + emits missing-safety-modifier diagnostic (downlevel).
src/libraries/System.Runtime.InteropServices/gen/Common/LibraryImportGeneratorExtensions.csAdds shared helpers to detect safety modifiers and the updated-rules feature flag.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/Strings.resxAdds strings for the new diagnostic message/description.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.de.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.es.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.fr.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.it.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ja.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ko.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pl.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pt-BR.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ru.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.tr.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hans.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hant.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.cs.xlfAdds localization entries for the new diagnostic.

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

CopilotAI 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

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

Copy link
Copy Markdown
MemberAuthor

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.

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

CopilotAI 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

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.

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

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

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

@EgorBo

EgorBo commented Jul 30, 2026

Copy link
Copy Markdown
MemberAuthor

PTAL @jtschuster@jkoritzinsky cc @jjonescz@333fred

  • Changed LibraryImportGenerator (to be precise: LibraryImportDiagnosticsAnalyzer and DownlevelLibraryImportDiagnosticsAnalyzer) to explicitly require safe or unsafe under the new rules with human-readable message on the user's side. It then is copied to the generated side. If extern is not directly forwarded, it's always internally marked as unsafe.
  • 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). - we then can audit and replace with safe.

- The SYSLIB1064 description no longer explains which shape the generator
emits. The requirement matches what the language asks of 'extern' members,
which is the part that matters to the reader.
- IL5007 stands down once the assembly is on the updated rules, where
SYSLIB1064 already reports the same methods. Its tests now run under the
legacy rules, which is the situation the analyzer exists for.
- The ConvertToLibraryImport fixer no longer drops an explicit 'safe' modifier.
DeclarationModifiers cannot represent it, so a declaration rebuilt through
it silently widened the method's contract to its callers.
- The downlevel analyzer has its own tests for the diagnostic, and the tests
reuse the feature flag constant rather than repeating the string.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 06c64005-a797-4517-9227-d1706eb5bca5
CopilotAI review requested due to automatic review settings July 30, 2026 20:32

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

@AaronRobinsonMSFTAaronRobinsonMSFT added reduce-unsafe source-generator Indicates an issue with a source generator feature and removed linkable-framework Issues associated with delivering a linker friendly framework labels Jul 30, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Jul 30, 2026
@AaronRobinsonMSFT

Copy link
Copy Markdown
Member

@jkoritzinsky PTAL.

@AaronRobinsonMSFTAaronRobinsonMSFT removed the linkable-framework Issues associated with delivering a linker friendly framework label Jul 30, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Jul 30, 2026
…unsafe-evolution
# Conflicts:
#	src/tools/illink/src/ILLink.CodeFix/Resources.resx
#	src/tools/illink/src/ILLink.CodeFix/UnsafeModifierCodeFixHelpers.cs
#	src/tools/illink/src/ILLink.RoslynAnalyzer/UnsafeMigrationSyntaxHelpers.cs

@jkoritzinskyjkoritzinsky left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One comment for forward-looking cleanup/style. Other than that, LGTM

Move the helpers into extension blocks whose members are named after the
Roslyn APIs they stand in for, so 'SyntaxKind.SafeKeyword' is spelled the way
the newer Roslyn declares it. A declared member wins over an extension member,
so once the reference is updated the call sites keep compiling unchanged and
the shim is a pure deletion.
The test project already references a Roslyn that declares the keyword, where
the call sites bind to the real member and need its experimental diagnostic
suppressed. The generators reference one that does not, and keep resolving the
kind at run time.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 06c64005-a797-4517-9227-d1706eb5bca5
CopilotAI review requested due to automatic review settings August 1, 2026 07:04

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

@EgorBo
EgorBo merged commit c251738 into dotnet:mainAug 1, 2026
112 of 114 checks passed
@EgorBo
EgorBo deleted the egorbo/libraryimport-unsafe-evolution branch August 1, 2026 16:06
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-rc1 milestone Aug 2, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Sep 2, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Runtime.InteropServiceslinkable-frameworkIssues associated with delivering a linker friendly frameworkreduce-unsafesource-generatorIndicates an issue with a source generator feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@EgorBo@jkotas@AaronRobinsonMSFT@jkoritzinsky@jjonescz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Support unsafe evolution in LibraryImportGenerator - #131245

Merged
EgorBo merged 4 commits into
dotnet:mainfrom
EgorBo:egorbo/libraryimport-unsafe-evolution
Aug 1, 2026
Merged

Support unsafe evolution in LibraryImportGenerator#131245
EgorBo merged 4 commits into
dotnet:mainfrom
EgorBo:egorbo/libraryimport-unsafe-evolution

Conversation

@EgorBo

@EgorBoEgorBo commented Jul 23, 2026

Copy link
Copy Markdown
Member

Tracking issue: #131451

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.

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

CopilotAI 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
FileDescription
src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/UnsafeCodeGeneration.csExpands shape tests to cover safe/unsafe mirroring and missing-modifier diagnostics under the feature flag.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.csprojLinks in the new shared extensions helper.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.csPasses the extracted safety modifier into wrapper-path inner DllImport local function generation.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/GeneratorDiagnostics.csAdds the new diagnostic descriptor for “missing safety modifier”.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/Analyzers/LibraryImportDiagnosticsAnalyzer.csIncludes + emits the new missing-safety-modifier diagnostic when the feature is enabled.
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/GeneratorDiagnostics.csAdds downlevel counterpart diagnostic descriptor.
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.csprojLinks in the new shared extensions helper.
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.csMirrors safety modifier onto wrapper-path inner DllImport local function (downlevel).
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportDiagnosticsAnalyzer.csIncludes + emits missing-safety-modifier diagnostic (downlevel).
src/libraries/System.Runtime.InteropServices/gen/Common/LibraryImportGeneratorExtensions.csAdds shared helpers to detect safety modifiers and the updated-rules feature flag.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/Strings.resxAdds strings for the new diagnostic message/description.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.de.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.es.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.fr.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.it.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ja.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ko.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pl.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pt-BR.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ru.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.tr.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hans.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hant.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.cs.xlfAdds localization entries for the new diagnostic.

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

CopilotAI 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

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

Copy link
Copy Markdown
MemberAuthor

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.

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

CopilotAI 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

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.

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

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

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

@EgorBo

EgorBo commented Jul 30, 2026

Copy link
Copy Markdown
MemberAuthor

PTAL @jtschuster@jkoritzinsky cc @jjonescz@333fred

  • Changed LibraryImportGenerator (to be precise: LibraryImportDiagnosticsAnalyzer and DownlevelLibraryImportDiagnosticsAnalyzer) to explicitly require safe or unsafe under the new rules with human-readable message on the user's side. It then is copied to the generated side. If extern is not directly forwarded, it's always internally marked as unsafe.
  • 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). - we then can audit and replace with safe.

- The SYSLIB1064 description no longer explains which shape the generator
emits. The requirement matches what the language asks of 'extern' members,
which is the part that matters to the reader.
- IL5007 stands down once the assembly is on the updated rules, where
SYSLIB1064 already reports the same methods. Its tests now run under the
legacy rules, which is the situation the analyzer exists for.
- The ConvertToLibraryImport fixer no longer drops an explicit 'safe' modifier.
DeclarationModifiers cannot represent it, so a declaration rebuilt through
it silently widened the method's contract to its callers.
- The downlevel analyzer has its own tests for the diagnostic, and the tests
reuse the feature flag constant rather than repeating the string.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 06c64005-a797-4517-9227-d1706eb5bca5
CopilotAI review requested due to automatic review settings July 30, 2026 20:32

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

@AaronRobinsonMSFTAaronRobinsonMSFT added reduce-unsafe source-generator Indicates an issue with a source generator feature and removed linkable-framework Issues associated with delivering a linker friendly framework labels Jul 30, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Jul 30, 2026
@AaronRobinsonMSFT

Copy link
Copy Markdown
Member

@jkoritzinsky PTAL.

@AaronRobinsonMSFTAaronRobinsonMSFT removed the linkable-framework Issues associated with delivering a linker friendly framework label Jul 30, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Jul 30, 2026
…unsafe-evolution
# Conflicts:
#	src/tools/illink/src/ILLink.CodeFix/Resources.resx
#	src/tools/illink/src/ILLink.CodeFix/UnsafeModifierCodeFixHelpers.cs
#	src/tools/illink/src/ILLink.RoslynAnalyzer/UnsafeMigrationSyntaxHelpers.cs

@jkoritzinskyjkoritzinsky left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One comment for forward-looking cleanup/style. Other than that, LGTM

Move the helpers into extension blocks whose members are named after the
Roslyn APIs they stand in for, so 'SyntaxKind.SafeKeyword' is spelled the way
the newer Roslyn declares it. A declared member wins over an extension member,
so once the reference is updated the call sites keep compiling unchanged and
the shim is a pure deletion.
The test project already references a Roslyn that declares the keyword, where
the call sites bind to the real member and need its experimental diagnostic
suppressed. The generators reference one that does not, and keep resolving the
kind at run time.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 06c64005-a797-4517-9227-d1706eb5bca5
CopilotAI review requested due to automatic review settings August 1, 2026 07:04

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

@EgorBo
EgorBo merged commit c251738 into dotnet:mainAug 1, 2026
112 of 114 checks passed
@EgorBo
EgorBo deleted the egorbo/libraryimport-unsafe-evolution branch August 1, 2026 16:06
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-rc1 milestone Aug 2, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Sep 2, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Runtime.InteropServiceslinkable-frameworkIssues associated with delivering a linker friendly frameworkreduce-unsafesource-generatorIndicates an issue with a source generator feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@EgorBo@jkotas@AaronRobinsonMSFT@jkoritzinsky@jjonescz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Support unsafe evolution in LibraryImportGenerator - #131245

Merged
EgorBo merged 4 commits into
dotnet:mainfrom
EgorBo:egorbo/libraryimport-unsafe-evolution
Aug 1, 2026
Merged

Support unsafe evolution in LibraryImportGenerator#131245
EgorBo merged 4 commits into
dotnet:mainfrom
EgorBo:egorbo/libraryimport-unsafe-evolution

Conversation

@EgorBo

@EgorBoEgorBo commented Jul 23, 2026

Copy link
Copy Markdown
Member

Tracking issue: #131451

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.

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

CopilotAI 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
FileDescription
src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/UnsafeCodeGeneration.csExpands shape tests to cover safe/unsafe mirroring and missing-modifier diagnostics under the feature flag.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.csprojLinks in the new shared extensions helper.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.csPasses the extracted safety modifier into wrapper-path inner DllImport local function generation.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/GeneratorDiagnostics.csAdds the new diagnostic descriptor for “missing safety modifier”.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/Analyzers/LibraryImportDiagnosticsAnalyzer.csIncludes + emits the new missing-safety-modifier diagnostic when the feature is enabled.
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/GeneratorDiagnostics.csAdds downlevel counterpart diagnostic descriptor.
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.csprojLinks in the new shared extensions helper.
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.csMirrors safety modifier onto wrapper-path inner DllImport local function (downlevel).
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportDiagnosticsAnalyzer.csIncludes + emits missing-safety-modifier diagnostic (downlevel).
src/libraries/System.Runtime.InteropServices/gen/Common/LibraryImportGeneratorExtensions.csAdds shared helpers to detect safety modifiers and the updated-rules feature flag.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/Strings.resxAdds strings for the new diagnostic message/description.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.de.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.es.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.fr.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.it.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ja.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ko.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pl.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pt-BR.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ru.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.tr.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hans.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hant.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.cs.xlfAdds localization entries for the new diagnostic.

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

CopilotAI 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

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

Copy link
Copy Markdown
MemberAuthor

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.

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

CopilotAI 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

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.

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

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

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

@EgorBo

EgorBo commented Jul 30, 2026

Copy link
Copy Markdown
MemberAuthor

PTAL @jtschuster@jkoritzinsky cc @jjonescz@333fred

  • Changed LibraryImportGenerator (to be precise: LibraryImportDiagnosticsAnalyzer and DownlevelLibraryImportDiagnosticsAnalyzer) to explicitly require safe or unsafe under the new rules with human-readable message on the user's side. It then is copied to the generated side. If extern is not directly forwarded, it's always internally marked as unsafe.
  • 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). - we then can audit and replace with safe.

- The SYSLIB1064 description no longer explains which shape the generator
emits. The requirement matches what the language asks of 'extern' members,
which is the part that matters to the reader.
- IL5007 stands down once the assembly is on the updated rules, where
SYSLIB1064 already reports the same methods. Its tests now run under the
legacy rules, which is the situation the analyzer exists for.
- The ConvertToLibraryImport fixer no longer drops an explicit 'safe' modifier.
DeclarationModifiers cannot represent it, so a declaration rebuilt through
it silently widened the method's contract to its callers.
- The downlevel analyzer has its own tests for the diagnostic, and the tests
reuse the feature flag constant rather than repeating the string.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 06c64005-a797-4517-9227-d1706eb5bca5
CopilotAI review requested due to automatic review settings July 30, 2026 20:32

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

@AaronRobinsonMSFTAaronRobinsonMSFT added reduce-unsafe source-generator Indicates an issue with a source generator feature and removed linkable-framework Issues associated with delivering a linker friendly framework labels Jul 30, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Jul 30, 2026
@AaronRobinsonMSFT

Copy link
Copy Markdown
Member

@jkoritzinsky PTAL.

@AaronRobinsonMSFTAaronRobinsonMSFT removed the linkable-framework Issues associated with delivering a linker friendly framework label Jul 30, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Jul 30, 2026
…unsafe-evolution
# Conflicts:
#	src/tools/illink/src/ILLink.CodeFix/Resources.resx
#	src/tools/illink/src/ILLink.CodeFix/UnsafeModifierCodeFixHelpers.cs
#	src/tools/illink/src/ILLink.RoslynAnalyzer/UnsafeMigrationSyntaxHelpers.cs

@jkoritzinskyjkoritzinsky left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One comment for forward-looking cleanup/style. Other than that, LGTM

Move the helpers into extension blocks whose members are named after the
Roslyn APIs they stand in for, so 'SyntaxKind.SafeKeyword' is spelled the way
the newer Roslyn declares it. A declared member wins over an extension member,
so once the reference is updated the call sites keep compiling unchanged and
the shim is a pure deletion.
The test project already references a Roslyn that declares the keyword, where
the call sites bind to the real member and need its experimental diagnostic
suppressed. The generators reference one that does not, and keep resolving the
kind at run time.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 06c64005-a797-4517-9227-d1706eb5bca5
CopilotAI review requested due to automatic review settings August 1, 2026 07:04

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

@EgorBo
EgorBo merged commit c251738 into dotnet:mainAug 1, 2026
112 of 114 checks passed
@EgorBo
EgorBo deleted the egorbo/libraryimport-unsafe-evolution branch August 1, 2026 16:06
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-rc1 milestone Aug 2, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Sep 2, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Runtime.InteropServiceslinkable-frameworkIssues associated with delivering a linker friendly frameworkreduce-unsafesource-generatorIndicates an issue with a source generator feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@EgorBo@jkotas@AaronRobinsonMSFT@jkoritzinsky@jjonescz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Support unsafe evolution in LibraryImportGenerator - #131245

Merged
EgorBo merged 4 commits into
dotnet:mainfrom
EgorBo:egorbo/libraryimport-unsafe-evolution
Aug 1, 2026
Merged

Support unsafe evolution in LibraryImportGenerator#131245
EgorBo merged 4 commits into
dotnet:mainfrom
EgorBo:egorbo/libraryimport-unsafe-evolution

Conversation

@EgorBo

@EgorBoEgorBo commented Jul 23, 2026

Copy link
Copy Markdown
Member

Tracking issue: #131451

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.

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

CopilotAI 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
FileDescription
src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/UnsafeCodeGeneration.csExpands shape tests to cover safe/unsafe mirroring and missing-modifier diagnostics under the feature flag.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.csprojLinks in the new shared extensions helper.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.csPasses the extracted safety modifier into wrapper-path inner DllImport local function generation.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/GeneratorDiagnostics.csAdds the new diagnostic descriptor for “missing safety modifier”.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/Analyzers/LibraryImportDiagnosticsAnalyzer.csIncludes + emits the new missing-safety-modifier diagnostic when the feature is enabled.
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/GeneratorDiagnostics.csAdds downlevel counterpart diagnostic descriptor.
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.csprojLinks in the new shared extensions helper.
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.csMirrors safety modifier onto wrapper-path inner DllImport local function (downlevel).
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportDiagnosticsAnalyzer.csIncludes + emits missing-safety-modifier diagnostic (downlevel).
src/libraries/System.Runtime.InteropServices/gen/Common/LibraryImportGeneratorExtensions.csAdds shared helpers to detect safety modifiers and the updated-rules feature flag.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/Strings.resxAdds strings for the new diagnostic message/description.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.de.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.es.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.fr.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.it.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ja.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ko.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pl.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pt-BR.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ru.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.tr.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hans.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hant.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.cs.xlfAdds localization entries for the new diagnostic.

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

CopilotAI 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

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

Copy link
Copy Markdown
MemberAuthor

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.

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

CopilotAI 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

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.

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

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

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

@EgorBo

EgorBo commented Jul 30, 2026

Copy link
Copy Markdown
MemberAuthor

PTAL @jtschuster@jkoritzinsky cc @jjonescz@333fred

  • Changed LibraryImportGenerator (to be precise: LibraryImportDiagnosticsAnalyzer and DownlevelLibraryImportDiagnosticsAnalyzer) to explicitly require safe or unsafe under the new rules with human-readable message on the user's side. It then is copied to the generated side. If extern is not directly forwarded, it's always internally marked as unsafe.
  • 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). - we then can audit and replace with safe.

- The SYSLIB1064 description no longer explains which shape the generator
emits. The requirement matches what the language asks of 'extern' members,
which is the part that matters to the reader.
- IL5007 stands down once the assembly is on the updated rules, where
SYSLIB1064 already reports the same methods. Its tests now run under the
legacy rules, which is the situation the analyzer exists for.
- The ConvertToLibraryImport fixer no longer drops an explicit 'safe' modifier.
DeclarationModifiers cannot represent it, so a declaration rebuilt through
it silently widened the method's contract to its callers.
- The downlevel analyzer has its own tests for the diagnostic, and the tests
reuse the feature flag constant rather than repeating the string.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 06c64005-a797-4517-9227-d1706eb5bca5
CopilotAI review requested due to automatic review settings July 30, 2026 20:32

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

@AaronRobinsonMSFTAaronRobinsonMSFT added reduce-unsafe source-generator Indicates an issue with a source generator feature and removed linkable-framework Issues associated with delivering a linker friendly framework labels Jul 30, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Jul 30, 2026
@AaronRobinsonMSFT

Copy link
Copy Markdown
Member

@jkoritzinsky PTAL.

@AaronRobinsonMSFTAaronRobinsonMSFT removed the linkable-framework Issues associated with delivering a linker friendly framework label Jul 30, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Jul 30, 2026
…unsafe-evolution
# Conflicts:
#	src/tools/illink/src/ILLink.CodeFix/Resources.resx
#	src/tools/illink/src/ILLink.CodeFix/UnsafeModifierCodeFixHelpers.cs
#	src/tools/illink/src/ILLink.RoslynAnalyzer/UnsafeMigrationSyntaxHelpers.cs

@jkoritzinskyjkoritzinsky left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One comment for forward-looking cleanup/style. Other than that, LGTM

Move the helpers into extension blocks whose members are named after the
Roslyn APIs they stand in for, so 'SyntaxKind.SafeKeyword' is spelled the way
the newer Roslyn declares it. A declared member wins over an extension member,
so once the reference is updated the call sites keep compiling unchanged and
the shim is a pure deletion.
The test project already references a Roslyn that declares the keyword, where
the call sites bind to the real member and need its experimental diagnostic
suppressed. The generators reference one that does not, and keep resolving the
kind at run time.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 06c64005-a797-4517-9227-d1706eb5bca5
CopilotAI review requested due to automatic review settings August 1, 2026 07:04

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

@EgorBo
EgorBo merged commit c251738 into dotnet:mainAug 1, 2026
112 of 114 checks passed
@EgorBo
EgorBo deleted the egorbo/libraryimport-unsafe-evolution branch August 1, 2026 16:06
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-rc1 milestone Aug 2, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Sep 2, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Runtime.InteropServiceslinkable-frameworkIssues associated with delivering a linker friendly frameworkreduce-unsafesource-generatorIndicates an issue with a source generator feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@EgorBo@jkotas@AaronRobinsonMSFT@jkoritzinsky@jjonescz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Support unsafe evolution in LibraryImportGenerator - #131245

Merged
EgorBo merged 4 commits into
dotnet:mainfrom
EgorBo:egorbo/libraryimport-unsafe-evolution
Aug 1, 2026
Merged

Support unsafe evolution in LibraryImportGenerator#131245
EgorBo merged 4 commits into
dotnet:mainfrom
EgorBo:egorbo/libraryimport-unsafe-evolution

Conversation

@EgorBo

@EgorBoEgorBo commented Jul 23, 2026

Copy link
Copy Markdown
Member

Tracking issue: #131451

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.

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

CopilotAI 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
FileDescription
src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/UnsafeCodeGeneration.csExpands shape tests to cover safe/unsafe mirroring and missing-modifier diagnostics under the feature flag.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.csprojLinks in the new shared extensions helper.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.csPasses the extracted safety modifier into wrapper-path inner DllImport local function generation.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/GeneratorDiagnostics.csAdds the new diagnostic descriptor for “missing safety modifier”.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/Analyzers/LibraryImportDiagnosticsAnalyzer.csIncludes + emits the new missing-safety-modifier diagnostic when the feature is enabled.
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/GeneratorDiagnostics.csAdds downlevel counterpart diagnostic descriptor.
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.csprojLinks in the new shared extensions helper.
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.csMirrors safety modifier onto wrapper-path inner DllImport local function (downlevel).
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportDiagnosticsAnalyzer.csIncludes + emits missing-safety-modifier diagnostic (downlevel).
src/libraries/System.Runtime.InteropServices/gen/Common/LibraryImportGeneratorExtensions.csAdds shared helpers to detect safety modifiers and the updated-rules feature flag.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/Strings.resxAdds strings for the new diagnostic message/description.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.de.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.es.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.fr.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.it.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ja.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ko.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pl.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pt-BR.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ru.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.tr.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hans.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hant.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.cs.xlfAdds localization entries for the new diagnostic.

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

CopilotAI 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

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

Copy link
Copy Markdown
MemberAuthor

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.

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

CopilotAI 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

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.

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

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

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

@EgorBo

EgorBo commented Jul 30, 2026

Copy link
Copy Markdown
MemberAuthor

PTAL @jtschuster@jkoritzinsky cc @jjonescz@333fred

  • Changed LibraryImportGenerator (to be precise: LibraryImportDiagnosticsAnalyzer and DownlevelLibraryImportDiagnosticsAnalyzer) to explicitly require safe or unsafe under the new rules with human-readable message on the user's side. It then is copied to the generated side. If extern is not directly forwarded, it's always internally marked as unsafe.
  • 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). - we then can audit and replace with safe.

- The SYSLIB1064 description no longer explains which shape the generator
emits. The requirement matches what the language asks of 'extern' members,
which is the part that matters to the reader.
- IL5007 stands down once the assembly is on the updated rules, where
SYSLIB1064 already reports the same methods. Its tests now run under the
legacy rules, which is the situation the analyzer exists for.
- The ConvertToLibraryImport fixer no longer drops an explicit 'safe' modifier.
DeclarationModifiers cannot represent it, so a declaration rebuilt through
it silently widened the method's contract to its callers.
- The downlevel analyzer has its own tests for the diagnostic, and the tests
reuse the feature flag constant rather than repeating the string.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 06c64005-a797-4517-9227-d1706eb5bca5
CopilotAI review requested due to automatic review settings July 30, 2026 20:32

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

@AaronRobinsonMSFTAaronRobinsonMSFT added reduce-unsafe source-generator Indicates an issue with a source generator feature and removed linkable-framework Issues associated with delivering a linker friendly framework labels Jul 30, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Jul 30, 2026
@AaronRobinsonMSFT

Copy link
Copy Markdown
Member

@jkoritzinsky PTAL.

@AaronRobinsonMSFTAaronRobinsonMSFT removed the linkable-framework Issues associated with delivering a linker friendly framework label Jul 30, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Jul 30, 2026
…unsafe-evolution
# Conflicts:
#	src/tools/illink/src/ILLink.CodeFix/Resources.resx
#	src/tools/illink/src/ILLink.CodeFix/UnsafeModifierCodeFixHelpers.cs
#	src/tools/illink/src/ILLink.RoslynAnalyzer/UnsafeMigrationSyntaxHelpers.cs

@jkoritzinskyjkoritzinsky left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One comment for forward-looking cleanup/style. Other than that, LGTM

Move the helpers into extension blocks whose members are named after the
Roslyn APIs they stand in for, so 'SyntaxKind.SafeKeyword' is spelled the way
the newer Roslyn declares it. A declared member wins over an extension member,
so once the reference is updated the call sites keep compiling unchanged and
the shim is a pure deletion.
The test project already references a Roslyn that declares the keyword, where
the call sites bind to the real member and need its experimental diagnostic
suppressed. The generators reference one that does not, and keep resolving the
kind at run time.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 06c64005-a797-4517-9227-d1706eb5bca5
CopilotAI review requested due to automatic review settings August 1, 2026 07:04

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

@EgorBo
EgorBo merged commit c251738 into dotnet:mainAug 1, 2026
112 of 114 checks passed
@EgorBo
EgorBo deleted the egorbo/libraryimport-unsafe-evolution branch August 1, 2026 16:06
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-rc1 milestone Aug 2, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Sep 2, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Runtime.InteropServiceslinkable-frameworkIssues associated with delivering a linker friendly frameworkreduce-unsafesource-generatorIndicates an issue with a source generator feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@EgorBo@jkotas@AaronRobinsonMSFT@jkoritzinsky@jjonescz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Support unsafe evolution in LibraryImportGenerator - #131245

Merged
EgorBo merged 4 commits into
dotnet:mainfrom
EgorBo:egorbo/libraryimport-unsafe-evolution
Aug 1, 2026
Merged

Support unsafe evolution in LibraryImportGenerator#131245
EgorBo merged 4 commits into
dotnet:mainfrom
EgorBo:egorbo/libraryimport-unsafe-evolution

Conversation

@EgorBo

@EgorBoEgorBo commented Jul 23, 2026

Copy link
Copy Markdown
Member

Tracking issue: #131451

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.

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

CopilotAI 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
FileDescription
src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/UnsafeCodeGeneration.csExpands shape tests to cover safe/unsafe mirroring and missing-modifier diagnostics under the feature flag.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.csprojLinks in the new shared extensions helper.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.csPasses the extracted safety modifier into wrapper-path inner DllImport local function generation.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/GeneratorDiagnostics.csAdds the new diagnostic descriptor for “missing safety modifier”.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/Analyzers/LibraryImportDiagnosticsAnalyzer.csIncludes + emits the new missing-safety-modifier diagnostic when the feature is enabled.
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/GeneratorDiagnostics.csAdds downlevel counterpart diagnostic descriptor.
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.csprojLinks in the new shared extensions helper.
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.csMirrors safety modifier onto wrapper-path inner DllImport local function (downlevel).
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportDiagnosticsAnalyzer.csIncludes + emits missing-safety-modifier diagnostic (downlevel).
src/libraries/System.Runtime.InteropServices/gen/Common/LibraryImportGeneratorExtensions.csAdds shared helpers to detect safety modifiers and the updated-rules feature flag.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/Strings.resxAdds strings for the new diagnostic message/description.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.de.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.es.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.fr.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.it.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ja.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ko.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pl.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pt-BR.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ru.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.tr.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hans.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hant.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.cs.xlfAdds localization entries for the new diagnostic.

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

CopilotAI 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

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

Copy link
Copy Markdown
MemberAuthor

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.

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

CopilotAI 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

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.

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

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

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

@EgorBo

EgorBo commented Jul 30, 2026

Copy link
Copy Markdown
MemberAuthor

PTAL @jtschuster@jkoritzinsky cc @jjonescz@333fred

  • Changed LibraryImportGenerator (to be precise: LibraryImportDiagnosticsAnalyzer and DownlevelLibraryImportDiagnosticsAnalyzer) to explicitly require safe or unsafe under the new rules with human-readable message on the user's side. It then is copied to the generated side. If extern is not directly forwarded, it's always internally marked as unsafe.
  • 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). - we then can audit and replace with safe.

- The SYSLIB1064 description no longer explains which shape the generator
emits. The requirement matches what the language asks of 'extern' members,
which is the part that matters to the reader.
- IL5007 stands down once the assembly is on the updated rules, where
SYSLIB1064 already reports the same methods. Its tests now run under the
legacy rules, which is the situation the analyzer exists for.
- The ConvertToLibraryImport fixer no longer drops an explicit 'safe' modifier.
DeclarationModifiers cannot represent it, so a declaration rebuilt through
it silently widened the method's contract to its callers.
- The downlevel analyzer has its own tests for the diagnostic, and the tests
reuse the feature flag constant rather than repeating the string.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 06c64005-a797-4517-9227-d1706eb5bca5
CopilotAI review requested due to automatic review settings July 30, 2026 20:32

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

@AaronRobinsonMSFTAaronRobinsonMSFT added reduce-unsafe source-generator Indicates an issue with a source generator feature and removed linkable-framework Issues associated with delivering a linker friendly framework labels Jul 30, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Jul 30, 2026
@AaronRobinsonMSFT

Copy link
Copy Markdown
Member

@jkoritzinsky PTAL.

@AaronRobinsonMSFTAaronRobinsonMSFT removed the linkable-framework Issues associated with delivering a linker friendly framework label Jul 30, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Jul 30, 2026
…unsafe-evolution
# Conflicts:
#	src/tools/illink/src/ILLink.CodeFix/Resources.resx
#	src/tools/illink/src/ILLink.CodeFix/UnsafeModifierCodeFixHelpers.cs
#	src/tools/illink/src/ILLink.RoslynAnalyzer/UnsafeMigrationSyntaxHelpers.cs

@jkoritzinskyjkoritzinsky left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One comment for forward-looking cleanup/style. Other than that, LGTM

Move the helpers into extension blocks whose members are named after the
Roslyn APIs they stand in for, so 'SyntaxKind.SafeKeyword' is spelled the way
the newer Roslyn declares it. A declared member wins over an extension member,
so once the reference is updated the call sites keep compiling unchanged and
the shim is a pure deletion.
The test project already references a Roslyn that declares the keyword, where
the call sites bind to the real member and need its experimental diagnostic
suppressed. The generators reference one that does not, and keep resolving the
kind at run time.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 06c64005-a797-4517-9227-d1706eb5bca5
CopilotAI review requested due to automatic review settings August 1, 2026 07:04

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

@EgorBo
EgorBo merged commit c251738 into dotnet:mainAug 1, 2026
112 of 114 checks passed
@EgorBo
EgorBo deleted the egorbo/libraryimport-unsafe-evolution branch August 1, 2026 16:06
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-rc1 milestone Aug 2, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Sep 2, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Runtime.InteropServiceslinkable-frameworkIssues associated with delivering a linker friendly frameworkreduce-unsafesource-generatorIndicates an issue with a source generator feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@EgorBo@jkotas@AaronRobinsonMSFT@jkoritzinsky@jjonescz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Support unsafe evolution in LibraryImportGenerator - #131245

Merged
EgorBo merged 4 commits into
dotnet:mainfrom
EgorBo:egorbo/libraryimport-unsafe-evolution
Aug 1, 2026
Merged

Support unsafe evolution in LibraryImportGenerator#131245
EgorBo merged 4 commits into
dotnet:mainfrom
EgorBo:egorbo/libraryimport-unsafe-evolution

Conversation

@EgorBo

@EgorBoEgorBo commented Jul 23, 2026

Copy link
Copy Markdown
Member

Tracking issue: #131451

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.

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

CopilotAI 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
FileDescription
src/libraries/System.Runtime.InteropServices/tests/LibraryImportGenerator.UnitTests/UnsafeCodeGeneration.csExpands shape tests to cover safe/unsafe mirroring and missing-modifier diagnostics under the feature flag.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.csprojLinks in the new shared extensions helper.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/LibraryImportGenerator.csPasses the extracted safety modifier into wrapper-path inner DllImport local function generation.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/GeneratorDiagnostics.csAdds the new diagnostic descriptor for “missing safety modifier”.
src/libraries/System.Runtime.InteropServices/gen/LibraryImportGenerator/Analyzers/LibraryImportDiagnosticsAnalyzer.csIncludes + emits the new missing-safety-modifier diagnostic when the feature is enabled.
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/GeneratorDiagnostics.csAdds downlevel counterpart diagnostic descriptor.
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.csprojLinks in the new shared extensions helper.
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportGenerator.csMirrors safety modifier onto wrapper-path inner DllImport local function (downlevel).
src/libraries/System.Runtime.InteropServices/gen/DownlevelLibraryImportGenerator/DownlevelLibraryImportDiagnosticsAnalyzer.csIncludes + emits missing-safety-modifier diagnostic (downlevel).
src/libraries/System.Runtime.InteropServices/gen/Common/LibraryImportGeneratorExtensions.csAdds shared helpers to detect safety modifiers and the updated-rules feature flag.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/Strings.resxAdds strings for the new diagnostic message/description.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.de.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.es.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.fr.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.it.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ja.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ko.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pl.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.pt-BR.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.ru.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.tr.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hans.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.zh-Hant.xlfAdds localization entries for the new diagnostic.
src/libraries/System.Runtime.InteropServices/gen/Common/Resources/xlf/Strings.cs.xlfAdds localization entries for the new diagnostic.

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

CopilotAI 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

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

Copy link
Copy Markdown
MemberAuthor

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.

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

CopilotAI 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

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.

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

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

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

@EgorBo

EgorBo commented Jul 30, 2026

Copy link
Copy Markdown
MemberAuthor

PTAL @jtschuster@jkoritzinsky cc @jjonescz@333fred

  • Changed LibraryImportGenerator (to be precise: LibraryImportDiagnosticsAnalyzer and DownlevelLibraryImportDiagnosticsAnalyzer) to explicitly require safe or unsafe under the new rules with human-readable message on the user's side. It then is copied to the generated side. If extern is not directly forwarded, it's always internally marked as unsafe.
  • 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). - we then can audit and replace with safe.

- The SYSLIB1064 description no longer explains which shape the generator
emits. The requirement matches what the language asks of 'extern' members,
which is the part that matters to the reader.
- IL5007 stands down once the assembly is on the updated rules, where
SYSLIB1064 already reports the same methods. Its tests now run under the
legacy rules, which is the situation the analyzer exists for.
- The ConvertToLibraryImport fixer no longer drops an explicit 'safe' modifier.
DeclarationModifiers cannot represent it, so a declaration rebuilt through
it silently widened the method's contract to its callers.
- The downlevel analyzer has its own tests for the diagnostic, and the tests
reuse the feature flag constant rather than repeating the string.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 06c64005-a797-4517-9227-d1706eb5bca5
CopilotAI review requested due to automatic review settings July 30, 2026 20:32

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

@AaronRobinsonMSFTAaronRobinsonMSFT added reduce-unsafe source-generator Indicates an issue with a source generator feature and removed linkable-framework Issues associated with delivering a linker friendly framework labels Jul 30, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Jul 30, 2026
@AaronRobinsonMSFT

Copy link
Copy Markdown
Member

@jkoritzinsky PTAL.

@AaronRobinsonMSFTAaronRobinsonMSFT removed the linkable-framework Issues associated with delivering a linker friendly framework label Jul 30, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Jul 30, 2026
…unsafe-evolution
# Conflicts:
#	src/tools/illink/src/ILLink.CodeFix/Resources.resx
#	src/tools/illink/src/ILLink.CodeFix/UnsafeModifierCodeFixHelpers.cs
#	src/tools/illink/src/ILLink.RoslynAnalyzer/UnsafeMigrationSyntaxHelpers.cs

@jkoritzinskyjkoritzinsky left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One comment for forward-looking cleanup/style. Other than that, LGTM

Move the helpers into extension blocks whose members are named after the
Roslyn APIs they stand in for, so 'SyntaxKind.SafeKeyword' is spelled the way
the newer Roslyn declares it. A declared member wins over an extension member,
so once the reference is updated the call sites keep compiling unchanged and
the shim is a pure deletion.
The test project already references a Roslyn that declares the keyword, where
the call sites bind to the real member and need its experimental diagnostic
suppressed. The generators reference one that does not, and keep resolving the
kind at run time.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 06c64005-a797-4517-9227-d1706eb5bca5
CopilotAI review requested due to automatic review settings August 1, 2026 07:04

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

@EgorBo
EgorBo merged commit c251738 into dotnet:mainAug 1, 2026
112 of 114 checks passed
@EgorBo
EgorBo deleted the egorbo/libraryimport-unsafe-evolution branch August 1, 2026 16:06
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-rc1 milestone Aug 2, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Sep 2, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Runtime.InteropServiceslinkable-frameworkIssues associated with delivering a linker friendly frameworkreduce-unsafesource-generatorIndicates an issue with a source generator feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@EgorBo@jkotas@AaronRobinsonMSFT@jkoritzinsky@jjonescz