Skip to content

[bgen] Propagate nullability information for generic type arguments. Fixes #16860#25541

Draft
rolfbjarne wants to merge 18 commits into
mainfrom
dev/rolf/bgen-generic-actions
Draft

[bgen] Propagate nullability information for generic type arguments. Fixes #16860#25541
rolfbjarne wants to merge 18 commits into
mainfrom
dev/rolf/bgen-generic-actions

Conversation

@rolfbjarne

@rolfbjarne rolfbjarne commented May 27, 2026

Copy link
Copy Markdown
Member

Summary

When properties have generic delegate types (like Action<NSObject?, NSError?>), bgen now correctly propagates the ? nullability annotations on the generic type arguments in the generated C# code.

Previously, bgen only read the first byte of the [NullableAttribute(byte[])] array (for the outer type's nullability), ignoring the remaining bytes that encode nullability for generic type arguments. This caused properties like Action<UIViewController?, NSError?> to be rendered as Action<UIViewController, NSError> — losing the nullability annotations.

Changes

  • src/bgen/AttributeManager.cs: Added GetNullabilityBytes() method that extracts the full byte array from [NullableAttribute].
  • src/bgen/TypeManager.cs: Added FormatType(Type?, Type?, byte[]?) overload and a recursive FormatTypeUsedIn helper that consumes nullability bytes in depth-first traversal order, correctly skipping value types (which don't have bytes in the array).
  • src/bgen/Generator.cs: Updated property rendering to pass nullability bytes. The "wrap" path applies nullability for all delegate types. The "non-wrap" path only applies for void-returning delegates (Action<>) to avoid Func<> covariance issues with trampoline signatures.
  • tests/bgen/BGenTests.cs: Added GenericTypeNullability test with 10 assertions.
  • tests/bgen/tests/generic-type-nullability.cs: Test input covering nullable args, non-nullable args, value types, many args, mixed patterns, and alternating nullability.

Key design decisions

  1. Value types skip byte consumption: The C# compiler does NOT emit bytes for value types in the NullableAttribute array. The fix correctly identifies value types and skips them during traversal.
  2. Func<> covariance guard: Trampolines use non-nullable generic args. This is fine for Action<in T> (contravariant) but breaks for Func<out T> (covariant). The non-wrap path only applies nullability for void-returning delegates.
  3. Wrap path is unrestricted: The wrap path generates self-contained code without trampoline interaction, so it can safely apply nullability for any delegate type.

Fixes #16860

🤖 Pull request created by Copilot

rolfbjarne and others added 3 commits May 27, 2026 14:16
…ixes #16860.

When a property has a generic type like Action<NSObject?, NSError?>, the
C# compiler emits a [NullableAttribute(byte[])] where each byte encodes
the nullability for the outer type and each generic argument in depth-first
order. Previously, bgen only looked at the first byte (the outer type) and
ignored nullability for generic type arguments.

This change:
- Adds GetNullabilityBytes() to AttributeManager to extract the full byte array
- Adds a FormatType overload in TypeManager that processes the byte array
  to annotate each generic type argument with ? where appropriate
- Uses the new method when rendering property type declarations

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…onsumption.

- Use braces for all if/else/for blocks in new code.
- Add complex test samples with many generic args, value types, and
  alternating nullability patterns.
- Fix bug where value types incorrectly consumed nullability bytes.
- Only apply nullability for void-returning delegates (Action<>) in the
  non-wrap path to avoid Func<> covariance issues with trampolines.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tead.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@rolfbjarne

Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@rolfbjarne

Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 3 pipeline(s).

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

rolfbjarne and others added 2 commits June 23, 2026 18:01
The MarkdownFormatter had the old and new text swapped in two places:
- DiffModification wrapped 'old' with addition markers and 'new' with
  removal markers (backwards compared to HtmlFormatter).
- The Diff method assigned the wrong Clean() arguments to the - and +
  lines, causing the old line to show added content and vice versa.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@vs-mobiletools-engineering-service2

Copy link
Copy Markdown
Collaborator

✅ [PR Build #60617e8] Build passed (Detect API changes) ✅

Pipeline on Agent
Hash: 60617e89b28dde5f5685110fc8ee2e3054aed75f [PR build]

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

Copy link
Copy Markdown
Collaborator

✅ [PR Build #60617e8] Build passed (Build packages) ✅

Pipeline on Agent
Hash: 60617e89b28dde5f5685110fc8ee2e3054aed75f [PR build]

@vs-mobiletools-engineering-service2

Copy link
Copy Markdown
Collaborator

✅ API diff for current PR / commit

NET (empty diffs)

✅ API diff vs stable

NET (empty diffs)

ℹ️ Generator diff

Generator Diff: vsdrops (html) vsdrops (raw diff) gist (raw diff) - Please review changes)

Pipeline on Agent
Hash: 60617e89b28dde5f5685110fc8ee2e3054aed75f [PR build]

@vs-mobiletools-engineering-service2

Copy link
Copy Markdown
Collaborator

✅ [PR Build #60617e8] Build passed (Build macOS tests) ✅

Pipeline on Agent
Hash: 60617e89b28dde5f5685110fc8ee2e3054aed75f [PR build]

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates bgen and related tooling to correctly propagate C# nullable-reference-type annotations (?) into generated signatures for generic type arguments (notably Action<T1,...> completion handlers), addressing #16860.

Changes:

  • bgen: extract full [Nullable] byte arrays and use them to render ? on generic type arguments in generated C# signatures (with a guard to avoid Func<> trampoline mismatches).
  • api-tools: render full nullability in mono-api-info outputs and improve markdown/html diff formatting for nullability-only changes.
  • tests: add a new bgen test input + assertions covering generic-argument nullability patterns (including value-type skipping).

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tools/api-tools/mono-api-info/mono-api-info.cs Formats member type names with full nullability (incl. generic args) and omits nullable metadata attributes from output.
tools/api-tools/mono-api-html/MultiplexedFormatter.cs Ensures placeholder </> tokens are replaced per-formatter before diff rendering.
tools/api-tools/mono-api-html/MarkdownFormatter.cs Fixes add/remove handling in modification diffs and handles nullability-only diff markers.
tools/api-tools/mono-api-html/ApiChange.cs Treats placeholder-escaped > as a valid separator when detecting ? suffixes.
tests/bgen/tests/generic-type-nullability.cs Adds binding input covering nullable/non-nullable generic args, value types, and mixed patterns.
tests/bgen/BGenTests.cs Adds a new test validating that generated output includes ? on the correct generic arguments.
src/Foundation/NSUrlSessionHandler.cs Updates redirection completion handler to accept nullable NSUrlRequest? and passes null directly.
src/foundation.cs Updates the delegate signature for redirection to Action<NSUrlRequest?> (scoped #nullable enable).
src/bgen/TypeManager.cs Adds formatting that consumes nullability-byte arrays to render nullable generic arguments.
src/bgen/Models/AsyncMethodInfo.cs Updates async-method analysis to infer nullable NSError? from the outer parameter’s nullability bytes.
src/bgen/Generator.cs Threads nullability-byte info into parameter/property signature rendering (with Func<> covariance guard).
src/bgen/AttributeManager.cs Adds GetNullabilityBytes to retrieve the full [Nullable] byte array.

Comment thread src/bgen/TypeManager.cs
Comment on lines +265 to +267
if (nullabilityBytes is null || nullabilityBytes.Length <= 1) {
return FormatTypeUsedIn (usedIn?.Namespace, type);
}
Comment thread src/bgen/TypeManager.cs
Comment on lines +492 to +495
// Reference types consume one byte from the array
byte currentByte = index < nullabilityBytes.Length ? nullabilityBytes [index] : (byte) 0;
index++;

Comment on lines +37 to +54
if (nullabilityBytes is not null && nullabilityBytes.Length > 1) {
// Walk the type arguments depth-first to find the byte index for the last param.
// Value types don't consume bytes, reference types do.
// byte[0] is for the Action<> itself, then each reference type arg consumes one byte.
int byteIndex = 1; // start after the outer type byte
var genericArgs = lastType.GetGenericArguments ();
for (int i = 0; i < genericArgs.Length; i++) {
if (!genericArgs [i].IsValueType) {
if (i == genericArgs.Length - 1) {
// This is the last generic argument (NSError)
IsNSErrorNullable = byteIndex < nullabilityBytes.Length && nullabilityBytes [byteIndex] == 2;
}
byteIndex++;
}
}
} else {
IsNSErrorNullable = generator.AttributeManager.IsNullable (lastParam);
}
@vs-mobiletools-engineering-service2

Copy link
Copy Markdown
Collaborator

🔥 [CI Build #60617e8] Test results 🔥

Test results

❌ Tests failed on VSTS: test results

0 tests crashed, 5 tests failed, 202 tests passed.

Failures

❌ fsharp tests

2 tests failed, 2 tests passed.

Failed tests

  • fsharp/iOS - simulator/Debug: LaunchTimedOut
  • fsharp/tvOS - simulator/Debug: LaunchTimedOut

Html Report (VSDrops) Download

❌ xcframework tests

2 tests failed, 2 tests passed.

Failed tests

  • xcframework-test/iOS - simulator/Debug: LaunchTimedOut
  • xcframework-test/tvOS - simulator/Debug: LaunchTimedOut

Html Report (VSDrops) Download

❌ Tests on macOS Sequoia (15) tests

1 tests failed, 4 tests passed.

Failed tests

  • monotouch-test: Failed (exit code 2)
    • AcceptSslCertificatesServicePointManager(System.Net.Http.HttpClientHandler) : Assert.That(ex, Is.Null)

Html Report (VSDrops) Download

Successes

✅ assembly-processing: All 1 tests passed. Html Report (VSDrops) Download
✅ cecil: All 1 tests passed. Html Report (VSDrops) Download
✅ dotnettests (iOS): All 1 tests passed. Html Report (VSDrops) Download
✅ dotnettests (MacCatalyst): All 1 tests passed. Html Report (VSDrops) Download
✅ dotnettests (macOS): All 1 tests passed. Html Report (VSDrops) Download
✅ dotnettests (Multiple platforms): All 1 tests passed. Html Report (VSDrops) Download
✅ dotnettests (tvOS): All 1 tests passed. Html Report (VSDrops) Download
✅ framework: All 2 tests passed. Html Report (VSDrops) Download
✅ generator: All 5 tests passed. Html Report (VSDrops) Download
✅ interdependent-binding-projects: All 4 tests passed. Html Report (VSDrops) Download
✅ introspection: All 4 tests passed. Html Report (VSDrops) Download
✅ linker (iOS): All 15 tests passed. Html Report (VSDrops) Download
✅ linker (MacCatalyst): All 15 tests passed. Html Report (VSDrops) Download
✅ linker (macOS): All 21 tests passed. Html Report (VSDrops) Download
✅ linker (tvOS): All 15 tests passed. Html Report (VSDrops) Download
✅ monotouch (iOS): All 20 tests passed. Html Report (VSDrops) Download
✅ monotouch (MacCatalyst): All 19 tests passed. Html Report (VSDrops) Download
✅ monotouch (macOS): All 20 tests passed. Html Report (VSDrops) Download
✅ monotouch (tvOS): All 20 tests passed. Html Report (VSDrops) Download
✅ msbuild: All 2 tests passed. Html Report (VSDrops) Download
✅ sharpie: All 1 tests passed. Html Report (VSDrops) Download
✅ windows: All 3 tests passed. Html Report (VSDrops) Download
✅ xtro: All 1 tests passed. Html Report (VSDrops) Download

macOS tests

✅ Tests on macOS Monterey (12): All 5 tests passed. Html Report (VSDrops) Download
✅ Tests on macOS Ventura (13): All 5 tests passed. Html Report (VSDrops) Download
✅ Tests on macOS Sonoma (14): All 5 tests passed. Html Report (VSDrops) Download
✅ Tests on macOS Tahoe (26): All 5 tests passed. Html Report (VSDrops) Download

Linux Build Verification

Linux build succeeded

Pipeline on Agent
Hash: 60617e89b28dde5f5685110fc8ee2e3054aed75f [PR build]

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[generator] Add support for nullable annotations in api definitions

3 participants