Skip to content

objc_msg_lookup_sender: type-qualifier skip discarded before the nil return-type switch — intended? #396

Description

@DTW-Thalion

While reading objc_msg_lookup_sender (Source sendmsg2.c) I noticed something in the nil-receiver handling that I wanted to check with you before treating it as a bug, since this lookup code is subtle and I may be missing an intended reason.

When the receiver is nil, the code selects a "zero slot" whose IMP returns the correctly-typed zero based on the method's return type:

const char *t = selector->types;
// Skip type qualifiers
while ('r' == *t || 'n' == *t || 'N' == *t || 'o' == *t ||
       'O' == *t || 'R' == *t || 'V' == *t || 'A' == *t)
{
    t++;
}
switch (selector->types[0])
{
    case 'D': return &nil_slot_D_v1;
    case 'd': return &nil_slot_d_v1;
    case 'f': return &nil_slot_f_v1;
}
return &nil_slot_v1;

The loop advances t past any leading type qualifiers, but the switch then reads selector->types[0] (the un-skipped first character), so t is computed and then discarded. For an unqualified return type the two are the same character, so it usually doesn't matter — but if the return type carries a qualifier prefix, the switch sees the qualifier rather than the type char and falls through to nil_slot_v1.

Is the intent to switch on the post-skip character (*t), or is there a reason the qualifier is deliberately kept here?

The reason it can matter in practice: clang does emit qualifier-prefixed encodings for floating-point return types carrying a bycopy / in / _Atomic qualifier:

  • - (double)d16@0:8
  • - (bycopy double)Od16@0:8
  • - (_Atomic double)Ad16@0:8

For such a method sent to nil through objc_msg_lookup_sender, the returned slot is nil_slot_v1 (IMP nil_method, whose signature is long long) instead of nil_slot_d_v1 (IMP nil_method_d). Since nil_method never writes the floating-point return register, [nil someBycopyDoubleMethod] comes back as a garbage double rather than 0.0.

Small reproduction, built against master:

@interface Bar : NSObject
- (double) plainD;
- (bycopy double) bcD;
@end
@implementation Bar
- (double) plainD { return 1.0; }
- (bycopy double) bcD { return 1.0; }
@end

id nilRcv = nil;
struct objc_slot *sp = objc_msg_lookup_sender(&nilRcv,
    method_getName(class_getInstanceMethod([Bar class], @selector(plainD))), nil);
struct objc_slot *sb = objc_msg_lookup_sender(&nilRcv,
    method_getName(class_getInstanceMethod([Bar class], @selector(bcD))), nil);

gives

plainD  d16@0:8   -> nil-slot IMP = nil_method_d
bcD     Od16@0:8  -> nil-slot IMP = nil_method   (different)

I appreciate this only bites for a fairly narrow combination — a qualifier-prefixed floating-point return, sent to nil, via the slot-based lookup (not the objc_msgSend fast path) — so it may be below the threshold you'd want to change. But since the qualifier skip looks like it was meant to feed the switch, I wanted to flag it and ask whether you'd like the three nil-return switches to use switch (*t), or whether it's written this way deliberately.

Thanks!

cc @davidchisnall

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions