Skip to content

[pigeon] Swift FFI wrapNumber serializes Objective-C CFBoolean / __NSCFBoolean inside Any as int instead of bool #193266

Closed
flutter/packages
#12997

Description

@tarrinneal

Description

When using Pigeon's Swift FFI generator (createWithNativeInteropApi()), any Objective-C boolean (__NSCFBoolean / CFBoolean, a subclass of NSNumber) boxed inside a Swift Any or [String: Any] (such as values returned from UserDefaults.object(forKey:) or UserDefaults.persistentDomain(forName:)) is serialized across FFI to Dart as an int (1 or 0) instead of a bool (true or false).

Root Cause

In packages/pigeon/lib/src/swift/swift_generator.dart (around line 2321), the generated wrapNumber(number: Any) helper switches on number with case let value as Int: before case let value as Bool::

private func wrapNumber(number: Any) -> MessagesNumberWrapper {
  switch number {
  case let value as Int:
    return MessagesNumberWrapper(number: NSNumber(value: value), type: 1)
  case let value as Int64:
    return MessagesNumberWrapper(number: NSNumber(value: value), type: 1)
  case let value as Double:
    return MessagesNumberWrapper(number: NSNumber(value: value), type: 2)
  case let value as Float:
    return MessagesNumberWrapper(number: NSNumber(value: value), type: 2)
  case let value as Bool:
    return MessagesNumberWrapper(number: NSNumber(value: value), type: 3)
  ...

When Foundation is imported in Swift, an NSNumber (including __NSCFBoolean) boxed in Any succeeds when dynamically cast with as? Int (bridging true to 1 and false to 0). Because case let value as Int: is evaluated first, any CFBoolean instance matches as Int (type: 1) and never reaches case let value as Bool: (type: 3).

(By contrast, when using standard platform channels, FlutterStandardCodec checks CFGetTypeID((__bridge CFTypeRef)value) == CFBooleanGetTypeID() before treating an NSNumber as an integer or floating-point number.)

Steps to Reproduce
Define a Pigeon @HostApi() method returning Object? or Map<String, Object> (for example, UserDefaultsApi.getValue or UserDefaultsApi.getAll in shared_preferences_foundation).
In the Swift implementation, return a boolean value retrieved from an Objective-C API such as UserDefaults.standard.object(forKey:) or UserDefaults.standard.persistentDomain(forName:).
Call the API from Dart using the FFI client (createWithNativeInteropApi()).
Observe that Dart receives 1 (int) instead of true (bool).
Expected Results
Booleans backed by CFBoolean / __NSCFBoolean inside Any should be wrapped with type: 3 (Bool) and decoded as bool in Dart, matching the behavior of FlutterStandardCodec.

Suggested Fix
Check CFGetTypeID(number as CFTypeRef) == CFBooleanGetTypeID() at the beginning of wrapNumber(number: Any) (or before checking Int / Double) in packages/pigeon/lib/src/swift/swift_generator.dart:

if CFGetTypeID(number as CFTypeRef) == CFBooleanGetTypeID(), let value = number as? Bool {
  return MessagesNumberWrapper(number: NSNumber(value: value), type: 3)
}

Activity

  1. self-assigned this
    on Sep 24, 2026
  2. added
    team-infraOwned by Infrastructure team
    p: pigeonrelated to pigeon messaging codegen tool
    on Sep 24, 2026
  3. github-actions commented on Oct 9, 2026

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

  4. locked as resolved and limited conversation to collaborators on Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

p: pigeonrelated to pigeon messaging codegen toolteam-infraOwned by Infrastructure team

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions