Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
f3903dd
[bgen] Propagate nullability information for generic type arguments. …
rolfbjarne May 27, 2026
1a89643
[bgen] Fix brace style, add complex test cases, fix value type byte c…
rolfbjarne May 27, 2026
bce4d13
[bgen] Remove null-forgiving operator usage, use pattern matching ins…
rolfbjarne May 27, 2026
54f622b
Merge branch 'main' into dev/rolf/bgen-generic-actions
rolfbjarne Jun 3, 2026
c0a4994
Merge branch 'main' into dev/rolf/bgen-generic-actions
rolfbjarne Jun 4, 2026
c85a3d9
[Foundation] Fix nullability for NSUrlSessionTaskDelegate.WillPerform…
rolfbjarne Jun 5, 2026
87b3249
Merge remote-tracking branch 'origin/main' into dev/rolf/bgen-generic…
rolfbjarne Jun 5, 2026
c698bc8
Fix build
rolfbjarne Jun 5, 2026
d37c395
Merge branch 'main' into dev/rolf/bgen-generic-actions
rolfbjarne Jun 16, 2026
c8cc48a
[bgen] Propagate nullability for generic type arguments in method par…
rolfbjarne Jun 17, 2026
d6e6634
[bgen] Add unit tests for nullability propagation in method parameter…
rolfbjarne Jun 17, 2026
58ffb9a
Merge remote-tracking branch 'origin/main' into dev/rolf/bgen-generic…
rolfbjarne Jun 17, 2026
7096ded
[api-tools] Fix API diff rendering issues
rolfbjarne Jun 17, 2026
42eca22
Merge branch 'main' into dev/rolf/bgen-generic-actions
rolfbjarne Jun 19, 2026
4778797
[api-tools] Render full generic type argument nullability in API info.
rolfbjarne Jun 19, 2026
92d1f45
Merge branch 'main' into dev/rolf/bgen-generic-actions
rolfbjarne Jun 22, 2026
f583af5
[api-tools] Fix reversed old/new in markdown API diff output.
rolfbjarne Jun 23, 2026
60617e8
Merge remote-tracking branch 'origin/main' into dev/rolf/bgen-generic…
rolfbjarne Jun 23, 2026
84feb12
[bgen] Handle single-byte NullableAttribute and fix depth-first byte …
rolfbjarne Jun 24, 2026
a294d33
Merge remote-tracking branch 'origin/main' into dev/rolf/bgen-generic…
rolfbjarne Jun 24, 2026
7661c4c
Auto-format source code
Jun 24, 2026
0404b11
Address review comments: array nullability in FormatType and markdown…
rolfbjarne Jun 24, 2026
58e9515
[api-tools] Preserve generic arity and handle % in nullability stripping
rolfbjarne Jun 24, 2026
8277b87
Merge remote-tracking branch 'origin/main' into dev/rolf/bgen-generic…
rolfbjarne Jun 24, 2026
d507026
[bgen] Fix array byte consumption in FormatType and remove null-forgi…
rolfbjarne Jun 24, 2026
27e95f8
Merge remote-tracking branch 'origin/main' into dev/rolf/bgen-generic…
rolfbjarne Jun 25, 2026
e725269
[bgen] Only use NullableAttribute bytes for generic delegates in Asyn…
rolfbjarne Jun 25, 2026
3edbdac
Remove unreachable System.Void check in FormatTypeUsedIn
rolfbjarne Jun 25, 2026
2a5fb53
Use nullability bytes consistently in wrap-property getter
rolfbjarne Jun 25, 2026
f576008
Add comment explaining DiffModification fix
rolfbjarne Jun 25, 2026
2512b80
Don't pass nullability bytes for array element type in NSArray.FromArray
rolfbjarne Jun 25, 2026
660de9c
Improve DiffModification comment to explain the original bug
rolfbjarne Jun 25, 2026
5b6276f
Propagate nullability to async wrapper return types
rolfbjarne Jun 25, 2026
db05a77
Auto-format source code
Jun 25, 2026
b0da7e8
Add bounds check for nullability byte slicing; simplify null check
rolfbjarne Jun 25, 2026
0839cab
Fix comments to describe the actual void-return-type check
rolfbjarne Jun 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/Foundation/NSUrlSessionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1036,17 +1036,17 @@ void WillCacheResponseImpl (NSUrlSession session, NSUrlSessionDataTask dataTask,
}

[Preserve (Conditional = true)]
public override void WillPerformHttpRedirection (NSUrlSession session, NSUrlSessionTask task, NSHttpUrlResponse response, NSUrlRequest newRequest, Action<NSUrlRequest> completionHandler)
public override void WillPerformHttpRedirection (NSUrlSession session, NSUrlSessionTask task, NSHttpUrlResponse response, NSUrlRequest newRequest, Action<NSUrlRequest?> completionHandler)
{
if (!sessionHandler.AllowAutoRedirect) {
completionHandler (null!);
completionHandler (null);
Comment thread
rolfbjarne marked this conversation as resolved.
return;
}

var inflight = GetInflightData (task);

if (inflight is null) {
completionHandler (null!);
completionHandler (null);
return;
}

Expand Down
40 changes: 40 additions & 0 deletions src/bgen/AttributeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -703,4 +703,44 @@ public bool IsNullable (ICustomAttributeProvider? provider)

return false;
}

// Returns the full nullability byte array from a [NullableAttribute], or null if not present.
// The byte array encodes nullability for the type and all its generic type arguments in
// depth-first traversal order:
// 0 = oblivious, 1 = not nullable, 2 = nullable
// For example, Action<NSObject?, NSError?> would have bytes [1, 2, 2].
public byte []? GetNullabilityBytes (ICustomAttributeProvider? provider)
{
var attributes = GetAttributes (provider);
if (attributes is null) {
return null;
}

foreach (var attrib in attributes) {
var attribType = attrib.GetAttributeType ();
if (attribType.Name == "NullableAttribute") {
if (attrib.ConstructorArguments.Count == 1) {
var argType = attrib.ConstructorArguments [0].ArgumentType;
if (argType.Namespace == "System" && argType.Name == "Byte") {
if (attrib.ConstructorArguments [0].Value is byte b) {
return new [] { b };
Comment thread
rolfbjarne marked this conversation as resolved.
}
}
if (argType.IsArray && argType.GetElementType ()?.Namespace == "System" && argType.GetElementType ()?.Name == "Byte") {
if (attrib.ConstructorArguments [0].Value is ReadOnlyCollection<CustomAttributeTypedArgument> valueCollection) {
Comment thread
rolfbjarne marked this conversation as resolved.
var result = new byte [valueCollection.Count];
for (int i = 0; i < valueCollection.Count; i++) {
if (valueCollection [i].Value is byte val) {
result [i] = val;
}
}
return result;
}
}
}
}
}

return null;
}
}
51 changes: 42 additions & 9 deletions src/bgen/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2927,16 +2927,27 @@ public void MakeSignatureFromParameterInfo (bool comma, StringBuilder sb, Member
if (!bt.IsValueType && AttributeManager.IsNullable (pi))
sb.Append ('?');
} else {
sb.Append (TypeManager.FormatType (declaringType, parType));
// Only apply nullability bytes for void-returning delegate types (Action<> variants).
// Func<> types have a covariant TResult which creates a type mismatch with the
// trampoline's CreateNullableBlock signature.
byte []? nullabilityBytes = null;
if (parType.IsSubclassOf (TypeCache.System_Delegate)) {
Comment thread
rolfbjarne marked this conversation as resolved.
var invokeMethod = parType.GetMethod ("Invoke");
if (invokeMethod is not null && invokeMethod.ReturnType == TypeCache.System_Void) {
Comment thread
rolfbjarne marked this conversation as resolved.
nullabilityBytes = AttributeManager.GetNullabilityBytes (pi);
}
}
sb.Append (TypeManager.FormatType (declaringType, parType, nullabilityBytes));
// some `IntPtr` are decorated with `[NullAttribute]`
if (!parType.IsValueType) {
if (AttributeManager.IsNullable (pi)) {
sb.Append ('?');
} else if (pi.Position == 0 && mi is MethodInfo minfo) {
// only need to check for setter, since we wouldn't get here for a getter.
var propertyInfo = GetProperty (minfo, getter: false, setter: true);
if (AttributeManager.IsNullable (propertyInfo))
if (AttributeManager.IsNullable (propertyInfo)) {
sb.Append ('?');
}
}
}
}
Expand Down Expand Up @@ -4029,10 +4040,11 @@ void GenerateProperty (Type type, PropertyInfo pi, List<string>? instance_fields
print_generated_code ();
PrintPropertyAttributes (pi, minfo);
PrintAttributes (pi, preserve: true, advice: true);
var wrapNullabilityBytes = AttributeManager.GetNullabilityBytes (pi);
print ("{0} {1}{2}{3} {4} {{",
mod,
minfo.GetModifiers (),
TypeManager.FormatType (pi.DeclaringType, pi.PropertyType),
TypeManager.FormatType (pi.DeclaringType, pi.PropertyType, wrapNullabilityBytes),
Comment thread
rolfbjarne marked this conversation as resolved.
nullable ? "?" : String.Empty,
pi.Name.GetSafeParamName ());
indent++;
Expand All @@ -4043,14 +4055,14 @@ void GenerateProperty (Type type, PropertyInfo pi, List<string>? instance_fields

if (TypeManager.IsDictionaryContainerType (pi.PropertyType)) {
print ("var src = {0} is not null ? new NSMutableDictionary ({0}) : null;", wrap);
print ("return src is null ? null! : new {0}(src);", TypeManager.FormatType (pi.DeclaringType, pi.PropertyType));
print ("return src is null ? null! : new {0}(src);", TypeManager.FormatType (pi.DeclaringType, pi.PropertyType, wrapNullabilityBytes));
} else {
if (TypeManager.IsArrayOfWrappedType (pi.PropertyType))
print ("return NSArray.FromArray<{0}>({1} as NSArray){2};", TypeManager.FormatType (pi.DeclaringType, pi.PropertyType.GetElementType ()), wrap, nullable ? "" : "!");
else if (pi.PropertyType.IsValueType)
print ("return ({0}) ({1});", TypeManager.FormatType (pi.DeclaringType, pi.PropertyType), wrap);
print ("return ({0}) ({1});", TypeManager.FormatType (pi.DeclaringType, pi.PropertyType, wrapNullabilityBytes), wrap);
else
print ("return ({0} as {1})!;", wrap, TypeManager.FormatType (pi.DeclaringType, pi.PropertyType));
print ("return ({0} as {1})!;", wrap, TypeManager.FormatType (pi.DeclaringType, pi.PropertyType, wrapNullabilityBytes));
}
indent--;
print ("}");
Expand Down Expand Up @@ -4115,7 +4127,17 @@ void GenerateProperty (Type type, PropertyInfo pi, List<string>? instance_fields
// it remains nullable only if the BindAs type can be null (i.e. a reference type)
nullable = !bindAsAttrib.Type.IsValueType && AttributeManager.IsNullable (pi);
} else {
propertyTypeName = TypeManager.FormatType (minfo.type, pi.PropertyType);
// Only apply nullability bytes for void-returning delegate types (Action<> variants).
// Func<> types have a covariant TResult which creates a type mismatch with the
// trampoline's CreateNullableBlock signature.
byte []? nullabilityBytes = null;
if (pi.PropertyType.IsSubclassOf (TypeCache.System_Delegate)) {
var invokeMethod = pi.PropertyType.GetMethod ("Invoke");
if (invokeMethod is not null && invokeMethod.ReturnType == TypeCache.System_Void) {
nullabilityBytes = AttributeManager.GetNullabilityBytes (pi);
}
}
propertyTypeName = TypeManager.FormatType (minfo.type, pi.PropertyType, nullabilityBytes);
}

print ("{0} {1}{2}{3} {4} {{",
Expand Down Expand Up @@ -4280,8 +4302,19 @@ string GetReturnType (AsyncMethodInfo minfo)
HashSet<string?>? reported1077;
string GetAsyncTaskType (AsyncMethodInfo minfo)
{
if (minfo.IsSingleArgAsync)
return TypeManager.FormatType (minfo.type, minfo.AsyncCompletionParams [0].ParameterType);
if (minfo.IsSingleArgAsync) {
var paramType = minfo.AsyncCompletionParams [0].ParameterType;
var paramBytes = minfo.CompletionParamNullabilityBytes? [0];
if (paramBytes is not null && !paramType.IsValueType) {
// Format with nullability for inner generic args
var formatted = TypeManager.FormatType (minfo.type, paramType, paramBytes);
// Check byte 0 of the slice for the param's own nullability
if (paramBytes [0] == 2)
formatted += "?";
return formatted;
}
return TypeManager.FormatType (minfo.type, paramType);
}

var attr = AttributeManager.GetOneCustomAttribute<AsyncAttribute> (minfo.mi);
if (attr.ResultTypeName is not null)
Expand Down
91 changes: 90 additions & 1 deletion src/bgen/Models/AsyncMethodInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ class AsyncMethodInfo : MemberInformation {
public bool IsVoidAsync { get; }
public bool IsSingleArgAsync { get; }
public MethodInfo MethodInfo { get; }
// Nullability bytes for each non-NSError completion parameter's type subtree.
// Each entry is a byte slice starting at that parameter's position in the
// delegate's NullableAttribute array (byte 0 = the param's own nullability).
// Null if nullability info is unavailable.
public byte []? []? CompletionParamNullabilityBytes { get; }

public AsyncMethodInfo (Generator generator, IMemberGatherer gather, Type type, MethodInfo mi, Type? categoryExtensionType, bool isExtensionMethod)
: base (generator, gather, mi, type, categoryExtensionType, false, isExtensionMethod)
Expand All @@ -26,14 +31,68 @@ public AsyncMethodInfo (Generator generator, IMemberGatherer gather, Type type,
AsyncCompletionParams = cbParams;

var lastParam = cbParams.LastOrDefault ();
var outerParam = mi.GetParameters ().Last ();
var genericArgs = lastType.GetGenericArguments ();
var nullabilityBytes = genericArgs.Length > 0 ? generator.AttributeManager.GetNullabilityBytes (outerParam) : null;

if (lastParam is not null && lastParam.ParameterType.Name == "NSError") {
HasNSError = true;
IsNSErrorNullable = generator.AttributeManager.IsNullable (lastParam);
// The nullability info for generic type arguments is encoded in the NullableAttribute
// on the outer method parameter (the one with the Action<...> type), not on the
// delegate's Invoke method parameters. Check the nullability bytes to determine if
// the NSError type argument is nullable.
// This only applies to generic delegate types (Action<...>). For non-generic delegates,
// the NullableAttribute on the outer parameter describes the delegate instance, not
// the Invoke parameters.
if (nullabilityBytes is not null && nullabilityBytes.Length == 1) {
// Single-byte (uniform) form: the same byte applies to all positions
IsNSErrorNullable = nullabilityBytes [0] == 2;
} else if (nullabilityBytes is not null && nullabilityBytes.Length > 1) {
// Multi-byte form: walk the type arguments depth-first to find the byte
// index for the last param (NSError). byte[0] is for the Action<> itself.
int byteIndex = 1;
for (int i = 0; i < genericArgs.Length; i++) {
if (i == genericArgs.Length - 1) {
// This is the last generic argument (NSError), which is a reference type
IsNSErrorNullable = byteIndex < nullabilityBytes.Length && nullabilityBytes [byteIndex] == 2;
break;
}
// Advance the byte index past this argument's subtree
byteIndex += CountNullabilityBytes (genericArgs [i]);
Comment thread
rolfbjarne marked this conversation as resolved.
}
} else {
IsNSErrorNullable = generator.AttributeManager.IsNullable (lastParam);
}
cbParams = cbParams.DropLast ();
}

IsVoidAsync = cbParams.Length == 0;
IsSingleArgAsync = cbParams.Length == 1;

// Compute nullability byte slices for each non-NSError completion param
if (nullabilityBytes is not null && genericArgs.Length > 0) {
var nonErrorArgCount = HasNSError ? genericArgs.Length - 1 : genericArgs.Length;
if (nonErrorArgCount > 0) {
CompletionParamNullabilityBytes = new byte [nonErrorArgCount] [];
if (nullabilityBytes.Length == 1) {
// Single-byte (uniform): every param gets the same byte
for (int i = 0; i < nonErrorArgCount; i++)
CompletionParamNullabilityBytes [i] = nullabilityBytes;
} else {
// Multi-byte: extract slices for each generic arg
int byteIndex = 1; // skip byte 0 (the Action<> itself)
for (int i = 0; i < nonErrorArgCount; i++) {
int paramByteCount = CountNullabilityBytes (genericArgs [i]);
if (byteIndex + paramByteCount > nullabilityBytes.Length)
break; // malformed or truncated attribute, skip remaining
var slice = new byte [paramByteCount];
Array.Copy (nullabilityBytes, byteIndex, slice, 0, paramByteCount);
CompletionParamNullabilityBytes [i] = slice;
byteIndex += paramByteCount;
}
}
}
}
}

public string GetUniqueParamName (string suggestion)
Expand All @@ -55,4 +114,34 @@ public string GetUniqueParamName (string suggestion)
}
}

// Counts how many nullability bytes a type subtree consumes in depth-first order.
// Value types consume 0 bytes (unless they are generic and contain reference type args).
// Reference types consume 1 byte for themselves, plus bytes for their generic args/element types.
static int CountNullabilityBytes (Type type)
Comment thread
rolfbjarne marked this conversation as resolved.
{
if (type.IsValueType) {
// Value types don't consume a byte themselves, but their generic args might
var vtargs = type.GetGenericArguments ();
int count = 0;
foreach (var arg in vtargs)
count += CountNullabilityBytes (arg);
return count;
}

// Reference types consume 1 byte for themselves
int bytes = 1;

if (type.IsArray) {
var elementType = type.GetElementType ();
if (elementType is not null)
bytes += CountNullabilityBytes (elementType);
} else {
var targs = type.GetGenericArguments ();
foreach (var arg in targs)
bytes += CountNullabilityBytes (arg);
}
Comment thread
Copilot marked this conversation as resolved.

return bytes;
}

}
Loading
Loading