How Protocols Work in mulle-objc: PROTOCOL vs SEL Explained

In mulle-objc, a PROTOCOL is a unique identifier representing a collection of method signatures stored as struct _mulle_objc_protocol, while a SEL (selector) is a 32-bit integer (mulle_objc_methodid_t) identifying a single method for dispatch—protocols describe interfaces, selectors drive method calls.

The mulle-objc runtime, implemented in the mulle-objc/mulle-objc-runtime repository, treats protocols and selectors as entirely separate identifier systems. While both compile down to unique integers, protocols track interface conformance through protocol identifiers (mulle_objc_protocolid_t), whereas selectors (SEL, aliased to mulle_objc_methodid_t) resolve to function pointers for message sending. Understanding this architectural split is critical for debugging conformance issues or extending the runtime.

What Is a Protocol in mulle-objc?

A protocol in mulle-objc is a compile-time contract that defines a set of methods a class must implement. Unlike selectors, which are ephemeral method names used during dispatch, protocols are persistent runtime objects registered in the universe.

Runtime Representation

When the compiler encounters @protocol, it emits a struct _mulle_objc_protocol defined in src/mulle-objc-protocol.h:

struct _mulle_objc_protocol {
    mulle_objc_protocolid_t protocolid;   // unique id generated from protocol name
    char *name;                           // textual name, e.g., "Baz"
};

This structure is stored in the protocol table of the universe (universe->protocoltable), a hash table populated during startup. The protocol identifier is a unique 32-bit integer derived from the protocol's name, distinct from method selectors.

Class-Pair Storage

Every class that adopts a protocol stores the protocol identifiers in its class-pair structure. In src/mulle-objc-classpair.h, the union p_protocolids holds a sorted array of mulle_objc_protocolid_t values:

union _mulle_objc_uniqueidarraypointer_t p_protocolids;   // array of protocolids

This array enables O(log n) conformance checks using binary search routines implemented in src/mulle-objc-protocol.c.

What Is a SEL in mulle-objc?

A selector (SEL) is simply an alias for mulle_objc_methodid_t, defined in src/mulle-objc-metaabi.h as a 32-bit integer. It represents a single method name used to look up an IMP (function pointer) in a class's method table.

Method Dispatch vs Protocol Conformance

Selectors power the message dispatch machinery found in src/mulle-objc-call.h. When you call [obj baz], the compiler emits:

SEL sel = @selector(baz);

The runtime then uses mulle_objc_method_lookup() to find the corresponding function pointer. There is no link between a protocolid and a methodid; a protocol merely documents which selectors a conforming class should implement. The selector table and protocol table are separate namespaces.

How Protocols Are Registered and Looked Up

During runtime initialization, every compiled struct _mulle_objc_protocol is hashed into the universe's protocol table. The lookup function in src/mulle-objc-universe.c provides direct access:

struct _mulle_objc_protocol *
_mulle_objc_universe_lookup_protocol(struct _mulle_objc_universe *universe,
                                    mulle_objc_protocolid_t protocolid);

This function retrieves the protocol structure by its identifier, useful for introspection or dynamic conformance checks.

Checking Protocol Conformance

To verify if a class adopts a protocol, the runtime inspects the class-pair's protocol array rather than the method tables.

The Conformance API

The primary API for conformance testing is declared in src/mulle-objc-object-convenience.h:

int _mulle_objc_infraclass_conformsto_protocolid(struct _mulle_objc_infraclass *cls,
                                                 mulle_objc_protocolid_t pid);

This function searches the class-pair's p_protocolids array (and superclasses) for the given identifier. In user code, you typically invoke this via the @protocol compiler directive:

if (_mulle_objc_infraclass_conformsto_protocolid(cls, @protocol(Baz))) {
    // Class conforms to Baz
}

Protocol Inheritance

Protocols can adopt other protocols. The compiler flattens the inheritance hierarchy into a single sorted array of protocol IDs stored in the adopting class's protocol-class. When checking conformance against @protocol(Foo), the runtime uses _mulle_objc_protocol_bsearch in src/mulle-objc-protocol.c to verify that all inherited protocol IDs are present, ensuring O(log n) performance even with deep inheritance chains.

Practical Code Examples

Declaring a Protocol and Adopting Class

// protocol_example.m
#import <mulle-objc-runtime/mulle-objc-runtime.h>

@protocol Baz
- (void) baz;
@end

@interface Foo <Baz>
@end

@implementation Foo
+ (id) new { return mulle_objc_infraclass_alloc_instance(self); }
- (void) dealloc { _mulle_objc_instance_free(self); }
- (void) baz { printf("Baz called\n"); }
@end

Runtime Conformance Check

int main(void)
{
    Foo *foo = [Foo new];
    struct _mulle_objc_infraclass *cls = mulle_objc_object_get_infraclass(foo);
    
    int conforms = _mulle_objc_infraclass_conformsto_protocolid(cls, @protocol(Baz));
    printf("Conforms to Baz? %s\n", conforms ? "YES" : "NO");
    
    [foo dealloc];
    return 0;
}

This test demonstrates the separation of concerns: @protocol(Baz) resolves to a mulle_objc_protocolid_t for the conformance check, while the actual method call [foo baz] uses a separate SEL lookup.

Using Selectors for Direct Dispatch

SEL sel = @selector(baz);
IMP imp = mulle_objc_method_lookup(cls, sel);
if (imp) {
    ((void (*)(id, SEL))imp)(foo, sel);
}

Here, sel is a mulle_objc_methodid_t, not a protocol identifier. This lookup bypasses protocol machinery entirely, going straight to the class's method table.

Summary

  • Protocols are runtime structures (struct _mulle_objc_protocol) identified by unique protocol IDs (mulle_objc_protocolid_t) stored in the universe's protocol table and class-pair arrays.
  • Selectors (SEL or mulle_objc_methodid_t) are 32-bit integers identifying single methods for dispatch, resolved via method tables unrelated to protocol storage.
  • Conformance checks use _mulle_objc_infraclass_conformsto_protocolid to search sorted protocol ID arrays in the class-pair, supporting O(log n) inheritance checks via _mulle_objc_protocol_bsearch.
  • The implementation spans mulle-objc-protocol.h, mulle-objc-classpair.h, and mulle-objc-universe.c for protocol handling, while mulle-objc-metaabi.h and mulle-objc-call.h manage selector dispatch.

Frequently Asked Questions

What is the difference between @protocol and @selector in mulle-objc?

@protocol generates a protocol identifier (mulle_objc_protocolid_t) used to verify interface conformance through structures stored in the universe's protocol table and class-pair arrays. @selector generates a method identifier (mulle_objc_methodid_t) used by the dispatch system in mulle-objc-call.h to locate function pointers. They occupy separate namespaces and serve different purposes: protocols describe contracts, selectors enable calls.

How does protocol inheritance work in mulle-objc?

When a protocol adopts another protocol (e.g., @protocol Foo <Bar>), the compiler generates a combined array of all required protocol IDs. This array is stored in the protocol-class's p_protocolids field. The runtime uses binary search (_mulle_objc_protocol_bsearch in src/mulle-objc-protocol.c) to check for inherited protocol membership, making conformance verification O(log n) regardless of inheritance depth.

Where are protocols stored in the mulle-objc runtime?

Protocol definitions reside in two locations: the universe's protocol table (universe->protocoltable in src/mulle-objc-universe.c) maps protocol IDs to struct _mulle_objc_protocol instances, while each class-pair (src/mulle-objc-classpair.h) stores a sorted array of adopted protocol IDs in the p_protocolids union. This dual storage enables both global protocol lookup and fast per-class conformance checks.

Can you convert a SEL to a protocol ID in mulle-objc?

No. SEL (mulle_objc_methodid_t) and mulle_objc_protocolid_t are distinct 32-bit identifier types with separate hash spaces and lookup tables. A selector identifies a single method for the dispatch engine, while a protocol ID identifies a collection of method signatures. There is no runtime API to convert between them because they represent orthogonal concepts—method implementation lookup versus interface contract verification.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →