Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why, Sending nil as parameters from Objc C to swift class initializer, replaces nil parameters with new objects

I created this Swift class:

@objc public class Tester: NSObject {
    private var name: String
    private var user: Users
    init(string:String, user: Users) {
        print(user.empId)
        print(user.name)
        self.user = user
        self.name = string
        super.init()
    }
}

I call the initializer from Obj C like this:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.

    NSString * nilString = nil;
    Users * nilUser = nil;
    Tester * test = [[Tester alloc] initWithString:nilString user:nilUser];

    return YES;
}

Here I pass nil for the parameters to the Swift initializer. Ideally I expect this to crash as the initializer only accepts non-nil values.

enter image description here

But what actually happens is, when the execution point reaches inside the initializer, new parameter objects are created:

enter image description here

The nil string becomes "" and the User variable that was nil is now pointing to an object.

But for a property like this

@property(nonatomic,strong) SomeClass * object;

where

object = nil;

When I call this object from swift,

let x = object.someGetter;

This crashes.

At one point, If I pass nil to some non null, it works and at another point, it crashes. Why does this weird behavior exist? If for some reasons, my parameters are nil, and passed to nonnull, I would like this to crash. So that I can fix things.

EDIT: This became so unexpected, further trying to play with this code.

enter image description here

The string parameter was actually as string, but the User shows uninitialized, hence manipulations on string worked well, but the user object, took the changes but did not showed them back.

like image 435
BangOperator Avatar asked Aug 31 '17 12:08

BangOperator


2 Answers

So there are three questions:

  • first, why does accessing the user properties not crash,
  • second, why is there an empty string instead of a nil one,
  • third, why does assigning a property (of a custom class) crash

I'll answer all of them :-)

1. Accessing the Users properties

Swift uses Objective C messaging when accessing the properties of the Users class, (I assume - Users is an ObjC-Class as seen in the debugger output; base class NSObject). In the disassembly view, one can see this:

0x1000018be <+78>:   movq   0x3e6e2b(%rip), %rsi      ; "empId"
   ....
0x1000018d7 <+103>:  callq  0x100361b10               ; symbol stub for: objc_msgSend

Since objc_msgSend supports nil messaging, the call does not fail.

2. Empty String magic

When calling the Swift initializer from Objective C, the bridging code creates the following:

0x100001f45 <+53>: callq  0x100021f50               
; static (extension in Foundation):
;Swift.String._unconditionallyBridgeFromObjectiveC (Swift.Optional<__ObjC.NSString>) -> Swift.String
   ....
0x100001f5b <+75>: callq  0x100001870               
; TestCalling.Tester.init (string : Swift.String, user : __ObjC.ObjCUser) -> TestCalling.Tester at SwiftClass.swift:14

The interesting part here is the _unconditionallyBridgeFromObjectiveC call. This will internally call the Swift.String function _cocoaStringToSwiftString_NonASCII, and checking the source code (here, line 48), you can see the following:

@inline(never) @_semantics("stdlib_binary_only") // Hide the CF dependency
func _cocoaStringToSwiftString_NonASCII(
  _ source: _CocoaString
) -> String {
  let cfImmutableValue = _stdlib_binary_CFStringCreateCopy(source)
  let length = _stdlib_binary_CFStringGetLength(cfImmutableValue)
  let start = _stdlib_binary_CFStringGetCharactersPtr(cfImmutableValue)

  return String(_StringCore(
    baseAddress: start,
    count: length,
    elementShift: 1,
    hasCocoaBuffer: true,
    owner: unsafeBitCast(cfImmutableValue, to: Optional<AnyObject>.self)))
}

The function always returns a new Swift.String object, in our case an empty one! So, again no crash.

3. Property access

When accessing a custom property, e.g. assigning it to a variable:

let x:SomeClass = object.someGetter;

the following happens:

  1. The return value of someGetter will be retained (objc_retainAutoreleasedReturnValue) -- this does not crash

  2. The returned object will be implicitly unwrapped -- which then crashes

If x were a weak property, or an optional, the code wouldn't crash. Even when using type inference, it does not crash (on my machine, swift 4):

let x = object.someGetter;

This is, because the inferred type of x is an optional SomeClass? unless the property itself is declared as nonnull:

@property(nonatomic,strong, nonnull) SomeClass * object;
like image 94
Andreas Oetjen Avatar answered Nov 10 '22 07:11

Andreas Oetjen


When you say the string is "" and the user is an object, it looks like you're just relying on the debugger's variable view for this, correct? Because the console output clearly shows nil twice.

What's happening here is the generated code for the initializer doesn't actually do anything that would crash if the values are nil. Semantically speaking, yes, it's invalid, but the actual machine code isn't doing something that relies on the non-nil assumption. The calls to print() work because, I assume, the code that converts the arguments into Any existentials happens to work even though the values are unexpectedly nil, and produce a nil Any.

As for the debugger view, it's a debugger, it's just doing the best it can to interpret the variables, but it's not actually running the Swift standard library. I'm not entirely sure why the string variable is showing up as "", I guess because it's always showing a string representation and it determines that it has no data (and a string with no data is ""). As for user, it actually shows it as 0x0000000000000000 which is correct, that means it's the null pointer (i.e. nil).

And then finally, let x = object.someGetter does crash, because the resulting code depends on the value being non-nil (specifically, it's presumably calling swift_retain() or swift_unknownRetain() on the value, which requires the argument to be non-nil as it dereferences the pointer).

like image 1
Lily Ballard Avatar answered Nov 10 '22 09:11

Lily Ballard