Resolve interfaces through Class, not per-instance pointers - #41
Conversation
Every instance struct carried its own copy of the interface pointer, one per level of its hierarchy, all holding the same value: clazz->interface. _alloc walked the superclass chain writing that pointer into each level's slot, and every ClassDef had to supply interfaceOffset to say where the slot was. The `$` macro never needed the stored value, only its type. The member is now a zero-length array, which carries the type for `typeof` and occupies no storage. `$` resolves the interface through Object::clazz on every call. interfaceOffset and the _alloc loop are gone. No call site changes. Because dispatch, cast, and isKindOfClass now all read the same field, reassigning clazz after allocation is sound: type checks and behavior change together. That permits Classes minted purely for behavior over an existing instance layout, which is what #23 asked for. Refs #23 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The PR introduces new/changed runtime behavior (re-classing) without a targeted regression test and the new documentation snippet’s callback signature is inconsistent with the referenced issue example.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR removes per-instance interface-pointer storage and makes all method dispatch resolve the interface through Object::clazz, reducing instance size and aligning type identity (clazz) with dispatch behavior ($). It also enables safe runtime “re-classing” of instances (proxy/mixin-style overrides) without stale per-instance interface pointers.
Changes:
- Replaces instance members like
FooInterface *interface;with zero-lengthFooInterface *interface[0];across core types and templates. - Removes
ClassDef.interfaceOffsetand the_allocloop that previously populated per-instance interface pointers; updates$to dispatch viaclassof(obj)->interface. - Updates documentation, templates, and Copilot guidance to reflect the new layout and the re-classing capability.
File summaries
| File | Description |
|---|---|
| Templates/Xcode/Objectively C Type.xctemplate/FILEBASENAME.h | Update generated instance struct to use interface[0] and adjust comment. |
| Templates/Xcode/Objectively C Type.xctemplate/FILEBASENAME.c | Remove generated .interfaceOffset from archetype definition. |
| Templates/Eclipse/eclipse-code-templates.xml | Update Eclipse templates to remove .interfaceOffset and use interface[0]. |
| Sources/Objectively/Class.h | Remove interfaceOffset from ClassDef; update $ macro dispatch docs + implementation. |
| Sources/Objectively/Class.c | Drop interfaceOffset assertion and remove per-instance interface population in _alloc. |
| Sources/Objectively/Object.h | Convert ObjectInterface *interface to ObjectInterface *interface[0]. |
| Sources/Objectively/Object.c | Remove .interfaceOffset from _Object archetype. |
| Sources/Objectively/Array.h | Convert ArrayInterface *interface to ArrayInterface *interface[0]. |
| Sources/Objectively/Array.c | Remove .interfaceOffset from _Array archetype. |
| Sources/Objectively/Boole.h | Convert BooleInterface *interface to BooleInterface *interface[0]. |
| Sources/Objectively/Boole.c | Remove .interfaceOffset from _Boole archetype. |
| Sources/Objectively/Condition.h | Convert ConditionInterface *interface to ConditionInterface *interface[0]. |
| Sources/Objectively/Condition.c | Remove .interfaceOffset from _Condition archetype. |
| Sources/Objectively/Data.h | Convert DataInterface *interface to DataInterface *interface[0]. |
| Sources/Objectively/Data.c | Remove .interfaceOffset from _Data archetype. |
| Sources/Objectively/Date.h | Convert DateInterface *interface to DateInterface *interface[0]. |
| Sources/Objectively/Date.c | Remove .interfaceOffset from _Date archetype. |
| Sources/Objectively/DateFormatter.h | Convert DateFormatterInterface *interface to DateFormatterInterface *interface[0]. |
| Sources/Objectively/DateFormatter.c | Remove .interfaceOffset from _DateFormatter archetype. |
| Sources/Objectively/Dictionary.h | Convert DictionaryInterface *interface to DictionaryInterface *interface[0]. |
| Sources/Objectively/Dictionary.c | Remove .interfaceOffset from _Dictionary archetype. |
| Sources/Objectively/Error.h | Convert ErrorInterface *interface to ErrorInterface *interface[0]. |
| Sources/Objectively/Error.c | Remove .interfaceOffset from _Error archetype. |
| Sources/Objectively/HashTable.h | Convert HashTableInterface *interface to HashTableInterface *interface[0]. |
| Sources/Objectively/HashTable.c | Remove .interfaceOffset from _HashTable archetype. |
| Sources/Objectively/IndexPath.h | Convert IndexPathInterface *interface to IndexPathInterface *interface[0]. |
| Sources/Objectively/IndexPath.c | Remove .interfaceOffset from _IndexPath archetype. |
| Sources/Objectively/IndexSet.h | Convert IndexSetInterface *interface to IndexSetInterface *interface[0]. |
| Sources/Objectively/IndexSet.c | Remove .interfaceOffset from _IndexSet archetype. |
| Sources/Objectively/JSONContext.h | Convert JSONContextInterface *interface to JSONContextInterface *interface[0]. |
| Sources/Objectively/JSONContext.c | Remove .interfaceOffset from _JSONContext archetype. |
| Sources/Objectively/JSONPath.h | Convert JSONPathInterface *interface to JSONPathInterface *interface[0]. |
| Sources/Objectively/JSONPath.c | Remove .interfaceOffset from _JSONPath archetype. |
| Sources/Objectively/List.h | Convert ListInterface *interface to ListInterface *interface[0]. |
| Sources/Objectively/List.c | Remove .interfaceOffset from _List archetype. |
| Sources/Objectively/Lock.h | Convert LockInterface *interface to LockInterface *interface[0]. |
| Sources/Objectively/Lock.c | Remove .interfaceOffset from _Lock archetype. |
| Sources/Objectively/Log.h | Convert LogInterface *interface to LogInterface *interface[0]. |
| Sources/Objectively/Log.c | Remove .interfaceOffset from _Log archetype. |
| Sources/Objectively/Null.h | Convert NullInterface *interface to NullInterface *interface[0]. |
| Sources/Objectively/Null.c | Remove .interfaceOffset from _Null archetype. |
| Sources/Objectively/Number.h | Convert NumberInterface *interface to NumberInterface *interface[0]. |
| Sources/Objectively/Number.c | Remove .interfaceOffset from _Number archetype. |
| Sources/Objectively/NumberFormatter.h | Convert NumberFormatterInterface *interface to NumberFormatterInterface *interface[0]. |
| Sources/Objectively/NumberFormatter.c | Remove .interfaceOffset from _NumberFormatter archetype. |
| Sources/Objectively/Operation.h | Convert OperationInterface *interface to OperationInterface *interface[0]. |
| Sources/Objectively/Operation.c | Remove .interfaceOffset from _Operation archetype. |
| Sources/Objectively/OperationQueue.h | Convert OperationQueueInterface *interface to OperationQueueInterface *interface[0]. |
| Sources/Objectively/OperationQueue.c | Remove .interfaceOffset from _OperationQueue archetype. |
| Sources/Objectively/Pointer.h | Convert PointerInterface *interface to PointerInterface *interface[0]. |
| Sources/Objectively/Pointer.c | Remove .interfaceOffset from _Pointer archetype. |
| Sources/Objectively/PointerArray.h | Convert PointerArrayInterface *interface to PointerArrayInterface *interface[0]. |
| Sources/Objectively/PointerArray.c | Remove .interfaceOffset from _PointerArray archetype. |
| Sources/Objectively/RESTClient.h | Convert RESTClientInterface *interface to RESTClientInterface *interface[0]. |
| Sources/Objectively/RESTClient.c | Remove .interfaceOffset from _RESTClient archetype. |
| Sources/Objectively/Regexp.h | Convert RegexpInterface *interface to RegexpInterface *interface[0]. |
| Sources/Objectively/Regexp.c | Remove .interfaceOffset from _Regexp archetype. |
| Sources/Objectively/Resource.h | Convert ResourceInterface *interface to ResourceInterface *interface[0]. |
| Sources/Objectively/Resource.c | Remove .interfaceOffset from _Resource archetype. |
| Sources/Objectively/Set.h | Convert SetInterface *interface to SetInterface *interface[0]. |
| Sources/Objectively/Set.c | Remove .interfaceOffset from _Set archetype. |
| Sources/Objectively/String.h | Convert StringInterface *interface to StringInterface *interface[0]. |
| Sources/Objectively/String.c | Remove .interfaceOffset from _String archetype. |
| Sources/Objectively/StringReader.h | Convert StringReaderInterface *interface to StringReaderInterface *interface[0]. |
| Sources/Objectively/StringReader.c | Remove .interfaceOffset from _StringReader archetype. |
| Sources/Objectively/Thread.h | Convert ThreadInterface *interface to ThreadInterface *interface[0]. |
| Sources/Objectively/Thread.c | Remove .interfaceOffset from _Thread archetype. |
| Sources/Objectively/URL.h | Convert URLInterface *interface to URLInterface *interface[0]. |
| Sources/Objectively/URL.c | Remove .interfaceOffset from _URL archetype. |
| Sources/Objectively/URLCache.h | Convert URLCacheInterface *interface to URLCacheInterface *interface[0]. |
| Sources/Objectively/URLCache.c | Remove .interfaceOffset from _URLCache archetype. |
| Sources/Objectively/URLCachedResponse.h | Convert URLCachedResponseInterface *interface to URLCachedResponseInterface *interface[0]. |
| Sources/Objectively/URLCachedResponse.c | Remove .interfaceOffset from _URLCachedResponse archetype. |
| Sources/Objectively/URLRequest.h | Convert URLRequestInterface *interface to URLRequestInterface *interface[0]. |
| Sources/Objectively/URLRequest.c | Remove .interfaceOffset from _URLRequest archetype. |
| Sources/Objectively/URLResponse.h | Convert URLResponseInterface *interface to URLResponseInterface *interface[0]. |
| Sources/Objectively/URLResponse.c | Remove .interfaceOffset from _URLResponse archetype. |
| Sources/Objectively/URLSession.h | Convert URLSessionInterface *interface to URLSessionInterface *interface[0]. |
| Sources/Objectively/URLSession.c | Remove .interfaceOffset from _URLSession archetype. |
| Sources/Objectively/URLSessionConfiguration.h | Convert URLSessionConfigurationInterface *interface to URLSessionConfigurationInterface *interface[0]. |
| Sources/Objectively/URLSessionConfiguration.c | Remove .interfaceOffset from _URLSessionConfiguration archetype. |
| Sources/Objectively/URLSessionDataTask.h | Convert URLSessionDataTaskInterface *interface to URLSessionDataTaskInterface *interface[0]. |
| Sources/Objectively/URLSessionDataTask.c | Remove .interfaceOffset from _URLSessionDataTask archetype. |
| Sources/Objectively/URLSessionDownloadTask.h | Convert URLSessionDownloadTaskInterface *interface to URLSessionDownloadTaskInterface *interface[0]. |
| Sources/Objectively/URLSessionDownloadTask.c | Remove .interfaceOffset from _URLSessionDownloadTask archetype. |
| Sources/Objectively/URLSessionTask.h | Convert URLSessionTaskInterface *interface to URLSessionTaskInterface *interface[0]. |
| Sources/Objectively/URLSessionTask.c | Remove .interfaceOffset from _URLSessionTask archetype. |
| Sources/Objectively/URLSessionUploadTask.h | Convert URLSessionUploadTaskInterface *interface to URLSessionUploadTaskInterface *interface[0]. |
| Sources/Objectively/URLSessionUploadTask.c | Remove .interfaceOffset from _URLSessionUploadTask archetype. |
| Sources/Objectively/Vector.h | Convert VectorInterface *interface to VectorInterface *interface[0]. |
| Sources/Objectively/Vector.c | Remove .interfaceOffset from _Vector archetype. |
| Examples/Hello.h | Update example type to use HelloInterface *interface[0]. |
| Examples/Hello.c | Remove .interfaceOffset from _Hello archetype. |
| Documentation/guide.md | Update type declaration guidance and add re-classing documentation. |
| .github/copilot/skills/new-type.md | Update “new type” guidance to interface[0] and remove .interfaceOffset. |
| .github/copilot-instructions.md | Update repo guidance to reflect new instance layout and dispatch mechanics. |
Review details
- Files reviewed: 96/96 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Dispatch and type identity both resolve through Object::clazz, so reassigning it must change what `$` calls immediately while the instance remains a kind of its original Class. Mint a Class from classof(object) at runtime, swap clazz, and assert both, then swap back. Guards against reintroducing a per-instance interface pointer. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Instance layout changed: the per-instance interface pointers are gone, so sizeof every type shrinks. Downstream libraries MUST be rebuilt against this release; ObjectivelyGPU and ObjectivelyMVC require >= 2.2.0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Replace hand-maintained libtool -version-info with -release MAJOR.MINOR, computed from AC_INIT. Every minor release gets a new soname, so a binary built against an older release fails to load rather than reading changed struct layouts; patch releases stay compatible. Nothing outside these repositories links Objectively, so there is no separately shipped consumer for current:revision:age to serve. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Adopt zero-length interface members from Objectively Objectively no longer stores an interface pointer on each instance; `$` resolves the interface through Object::clazz, and the struct member exists only to carry a type for typeof. ClassDef.interfaceOffset is gone. Follow suit: each instance struct's interface member becomes a zero-length array, and each ClassDef drops interfaceOffset. No call sites change. See jdolan/Objectively#41. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Bump version to 2.2.0; require Objectively >= 2.2.0 Objectively 2.2.0 changed the instance layout (no per-instance interface pointers), so this library MUST be built against it and nothing older. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Derive the shared library soname from the package version Replace the -version-info hard-coded in Makefile.am with -release MAJOR.MINOR, computed from AC_INIT, so the soname follows configure.ac. Every minor release gets a new soname; patch releases stay compatible. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
) * Adopt zero-length interface members from Objectively Objectively no longer stores an interface pointer on each instance; `$` resolves the interface through Object::clazz, and the struct member exists only to carry a type for typeof. ClassDef.interfaceOffset is gone. Follow suit: each instance struct's interface member becomes a zero-length array, and each ClassDef drops interfaceOffset. No call sites change. See jdolan/Objectively#41 and jdolan/ObjectivelyGPU#6. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Bump version to 2.2.0; require ObjectivelyGPU and Objectively >= 2.2.0 Objectively 2.2.0 changed the instance layout (no per-instance interface pointers); ObjectivelyGPU 2.2.0 is the first build against it. This library MUST be built against both and nothing older. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Derive the shared library soname from the package version The library was previously unversioned. Use -release MAJOR.MINOR, computed from AC_INIT, so every minor release gets a new soname and patch releases stay compatible, matching Objectively and ObjectivelyGPU. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Fix Makefile.am by correcting LDFLAGS syntax Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Why
Every instance struct carried a typed interface pointer —
StringInterface *interface, and so on — one per level of its hierarchy. They were all copies of the same value,clazz->interface:_allocwalked the superclass chain writing that one pointer into each level's slot, and everyClassDefhad to supplyinterfaceOffsetso it knew where the slot was.That was the one place Objectively kept two independent sources of truth about an instance's type:
clazz(read bycast,instanceof,isKindOfClass) andinterface(read by$). They were populated together at_allocand never reconciled afterward.What
The
$macro never needed the stored value — only its type, whichtypeof(obj->interface)provided because the struct declared a member of that type. So the member is now a zero-length array:and
$resolves the interface throughObject::clazzon every call:Removed:
ClassDef.interfaceOffset, the_allocpopulation loop, and one pointer per hierarchy level from every instance (sizeof(Object)32 → 24).No call site changes anywhere.
Tests/is untouched and passes as-is, which is the proof. Downstream (GPU, MVC, Quetoo) needs exactly two mechanical,sed-safe edits per class:Interface *interface;→Interface *interface[0];and delete.interfaceOffset = offsetof(...).What it enables: mixins and dynamic overrides
$,cast, andinstanceof/isKindOfClassnow resolve through the same field,Object::clazz, andinterfaceof()is the single primitive under$,$$, andsuper. One consequence: reassigningclazzafter allocation is now sound — type identity and behavior change together, immediately. Previouslyinstanceof(Proxy, obj)would have said yes while every$(obj, …)kept dispatching through the original class's stale pointer.That makes a Class minted purely for behavior, over an instance that already exists, viable — no struct, header, or archetype of its own (this is what #23 asked for):
The proxy inherits every method (
_initializestillmemcpys the superclass interface first) and overrides only what itsinitializesets. The same mechanism gives you mixin-style behavior layering, test spies/mocks, and KVO-style observation without touching the original type. This needed both halves of the change: withinterfaceOffsetgone, a Class can be built from a runtimeClass *alone with no compile-time type name; with dispatch readingclazz, the swap actually takes effect. Documented inDocumentation/guide.md("Re-classing an instance") and in the$doc block.Verification
makeclean (0 warnings),make check26/26 pass withTests/unchanged.Examples/HelloandExamples/HelloCpp(C++ consumer of the same headers/macro) build and run.constreceivers, overrides reached through an ancestor static type, inherited methods, and live re-classing.git grepfinds no remaininginterfaceOffsetor old-style member. Xcode/Eclipse templates and Copilot guidance updated.Caveats
typeof,({ }), and, ## __VA_ARGS__this codebase already requires, so no new portability class. Verified with Apple clang (gnu11,gnu23,gnu++17); no real GCC was available locally, so Linux CI is the real proof.sizeofof every instance changes, so binaries built against old and new headers must not exchange raw instances. Nothing in these repos does; everything goes throughinstanceSize.interfacemember MUST directly follow the parent member: it has pointer alignment, so placing it after a smaller field would introduce padding.Refs #23
🤖 Generated with Claude Code