Skip to content

[linker] Add linker step to remove calls to Console.WriteLine - #26248

Open
Adham-Kiwan wants to merge 1 commit into
dotnet:mainfrom
Adham-Kiwan:linker-remove-console-writeline
Open

[linker] Add linker step to remove calls to Console.WriteLine#26248
Adham-Kiwan wants to merge 1 commit into
dotnet:mainfrom
Adham-Kiwan:linker-remove-console-writeline

Conversation

@Adham-Kiwan

Copy link
Copy Markdown

Fixes #16781.

Summary

Adds a new opt-in linker optimization, remove-console-writeline, that strips calls to System.Console.WriteLine from application code during trimming/linking, to help reduce shipped app size. As noted in the issue, these calls (and any string-formatting logic used to build their arguments) are typically dead weight once an app has shipped, since there's usually nobody around to read a mobile app's standard output.

What's included

  • tools/dotnet-linker/RemoveConsoleWriteLineCallsStep.cs (new): a custom linker step, subclassing AssemblyModifierStep, that walks method bodies looking for calls to Console.WriteLine and removes them:
    • The call instruction itself is turned into a nop (rather than removed outright), so that any branches or exception handler regions that reference that exact instruction stay valid - this mirrors the existing Nop helper pattern in tools/linker/OptimizeGeneratedCode.cs.
    • Each argument is replaced with a pop instruction instead of also being nop'ed out. This keeps the evaluation stack balanced and preserves any side effects from computing the arguments (e.g. Console.WriteLine(ComputeSomething()) still calls ComputeSomething(), it just no longer prints the result). Only the actual write to the console is removed.
  • tools/common/Optimizations.cs: registers remove-console-writeline as a new named optimization, following the existing --optimize=[+|-]<name> mechanism shared by mtouch/mmp/dotnet builds. It's opt-in on every platform (not enabled by default anywhere), because - unlike the other optimizations in this file - it changes the observable behavior of the app (visible console output disappears). This can be revisited if maintainers would prefer a default-on-for-release behavior instead, per the issue's framing.
  • dotnet/targets/Xamarin.Shared.Sdk.targets: registers the new step in the trimmer pipeline (BeforeStep="MarkStep", gated only on _AreAnyAssembliesTrimmed), right next to OptimizeGeneratedCodeStep. The step is always registered when assemblies are trimmed, but internally no-ops unless the optimization was explicitly turned on (same pattern OptimizeGeneratedCodeStep uses for its own sub-optimizations).
  • docs/website/optimizations.md: documents the new optimization in the same format as the existing entries on that page.
  • tests/mtouch/MTouch.cs: updates the MT0132 test's expected "valid optimizations" list to include the new name (it's an exhaustive list built from Optimizations.opt_names, so adding a new optimization requires this one-line update).

Prior art followed

Scope decisions

  • Only Console.WriteLine is targeted, not the other Console.Write* members (Console.Write, writing directly via Console.Out, etc.), per the issue's suggestion to keep scope tight. Extending to the rest of the family should be a small, mechanical follow-up (the mr.Name / DeclaringType check would just need a couple more cases) if there's interest.
  • The optimization is off by default everywhere. I went with opt-in rather than "on by default for Release" because it's a user-visible behavior change (removes real output), and I'd rather have that be a deliberate choice than a surprise. Happy to flip the default if maintainers disagree.

Test plan - please read carefully

I was not able to build or run this repository's actual build/test suite in my environment, per the task constraints: this repo requires a full Xcode + mono/.NET-for-Apple bootstrap (make bootstrap), which pins a specific preview .NET SDK via global.json (10.0.400-preview.0.26371.110, fetched via make dotnet -C builds) and isn't available here. I did not attempt it, and I want to be explicit that the actual dotnet-linker.csproj was never compiled, and the new step was never run through real illink/the real trimmer pipeline, or against a real app.

What I did do to gain confidence in the change, given that constraint:

  1. Read the existing custom-linker-step infrastructure closely (tools/dotnet-linker/*.cs, tools/dotnet-linker/Steps/*.cs, tools/linker/OptimizeGeneratedCode.cs, and the _TrimmerCustomSteps wiring in Xamarin.Shared.Sdk.targets) to match the exact conventions (base class, namespace, file placement, Name/ErrorCode members, registration style, AreAnyAssembliesTrimmed/PrepareAssemblies gating).
  2. Verified the core IL-rewriting algorithm in isolation, since it's the riskiest part of this change (stack-balance bugs here would produce an unverifiable/crashing assembly): I copied the exact ProcessMethod/RemoveCall logic from the new step into a standalone console app referencing the Mono.Cecil NuGet package directly (outside this repo, since a plain dotnet build/dotnet run was usable there without the pinned SDK), then:
    • Compiled a small sample assembly exercising several Console.WriteLine call shapes: zero args, one string arg, an interpolated string (which compiles to the DefaultInterpolatedStringHandler pattern - the trickiest case, since the actual WriteLine call only takes the final formatted string), a multi-argument format overload (WriteLine(string, object, object)), and an argument with an observable side effect (a method call that also writes to a log file).
    • Ran the rewriter against it, dumped the resulting IL, and confirmed the callnop + pop-per-argument rewrite looked correct for every shape above.
    • Actually executed the rewritten assembly on the real .NET 10 runtime: confirmed it ran to completion with no InvalidProgramException/verification failure (i.e. the stack balance is correct), produced zero console output (all Console.WriteLine calls were successfully elided), and that the side-effecting argument still ran exactly once (confirmed via its log file) - i.e. argument side effects are preserved as intended.
  3. Compiled the new step file itself against a set of hand-written stub types that reproduce the exact method/property signatures of the real base classes (AssemblyModifierStep, ConfigurationAwareStep, Mono.Tuner.Extensions.Is, Mono.Linker.AssemblyAction, etc., copied from what I read in this repo) to catch any compile-time mistakes in the class itself. This compiled cleanly, but it is not a substitute for compiling against the real base classes in dotnet-linker.csproj, since I could have subtly misread a signature.

What this means concretely: I'm confident the IL-rewriting logic is correct (it's been exercised end-to-end on real IL and a real runtime), and I'm confident the step follows this codebase's conventions closely (by close reading), but the integration point - the actual custom-step registration loading correctly into illink, LinkerConfiguration/Optimizations plumbing end-to-end, and a real app build with --optimize=+remove-console-writeline - has not been exercised, and I'd very much appreciate maintainer/CI validation on that front before merge. If CI surfaces a wiring mistake (e.g. a namespace/type-name typo in the _TrimmerCustomSteps entry), it should be a quick fix.

  • Maintainer/CI: confirm the step builds as part of dotnet-linker.csproj.
  • Maintainer/CI: confirm --optimize=+remove-console-writeline actually removes Console.WriteLine output from a real trimmed app build.
  • Maintainer/CI: confirm the default (off) behavior doesn't change existing app output.

🤖 Generated with Claude Code

…otnet#16781)

Adds a new opt-in linker optimization, `remove-console-writeline`, that
strips calls to System.Console.WriteLine from application code during
trimming, to help reduce shipped app size (these calls - and any string
formatting feeding them - are typically dead weight once an app has
shipped).

- tools/dotnet-linker/RemoveConsoleWriteLineCallsStep.cs: new
  AssemblyModifierStep (same base class/conventions as
  OptimizeGeneratedCodeStep) that scans method bodies for calls to
  Console.WriteLine and nops them out. Call arguments are replaced with
  'pop' instructions (one per argument) instead of also being nop'ed
  out, so that any side effects from evaluating them (e.g. a method
  call passed as an argument) are preserved - only the actual write to
  the console is removed. Scope is intentionally limited to
  Console.WriteLine (not the other Console.Write* members), to keep
  the initial change small; this can be broadened later if needed.

- tools/common/Optimizations.cs: registers the new
  `remove-console-writeline` optimization (opt-in only, not enabled by
  default on any platform, since - unlike the other optimizations here -
  it changes the observable behavior of the app).

- dotnet/targets/Xamarin.Shared.Sdk.targets: registers the new step in
  the linker pipeline (BeforeStep="MarkStep"), following the same
  pattern as OptimizeGeneratedCodeStep.

- docs/website/optimizations.md: documents the new optimization,
  following the existing format for this file.

- tests/mtouch/MTouch.cs: updates the MT0132 test's expected message to
  include the new optimization name in the list of valid optimizations.

Modeled on tools/dotnet-linker/OptimizeGeneratedCodeStep.cs (registration/
base class conventions) and tools/linker/OptimizeGeneratedCode.cs (IL
nop-ing pattern), which are the existing custom linker steps that rewrite
method bodies in this codebase.

See the PR description for the test plan and what could/couldn't be
verified in this environment (the full dotnet/macios bootstrap - Xcode,
the pinned preview .NET SDK, etc. - was not available).
@Adham-Kiwan
Adham-Kiwan requested a review from rolfbjarne as a code owner July 22, 2026 18:23
@rolfbjarne rolfbjarne added the community Community contribution ❤ label Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community Community contribution ❤

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add linker step to remove calls to Console.WriteLine

2 participants