Skip to content

Resolve interfaces through Class, not per-instance pointers - #41

Merged
jdolan merged 5 commits into
mainfrom
zero-length-interface
Sep 2, 2026
Merged

Resolve interfaces through Class, not per-instance pointers#41
jdolan merged 5 commits into
mainfrom
zero-length-interface

Conversation

@jdolan

@jdolan jdolan commented Sep 2, 2026

Copy link
Copy Markdown
Owner

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: _alloc walked the superclass chain writing that one pointer into each level's slot, and every ClassDef had to supply interfaceOffset so 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 by cast, instanceof, isKindOfClass) and interface (read by $). They were populated together at _alloc and never reconciled afterward.

What

The $ macro never needed the stored value — only its type, which typeof(obj->interface) provided because the struct declared a member of that type. So the member is now a zero-length array:

struct String {
  Object object;
  StringInterface *interface[0];   // carries a type, occupies no storage
  char *chars;
  ...

and $ resolves the interface through Object::clazz on every call:

#define $(obj, method, ...) \
  ({ \
    typeof(obj) _obj = (obj); \
    ((typeof(_obj->interface[0])) classof(_obj)->interface)->method(_obj, ## __VA_ARGS__); \
  })

Removed: ClassDef.interfaceOffset, the _alloc population 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, and instanceof/isKindOfClass now resolve through the same field, Object::clazz, and interfaceof() is the single primitive under $, $$, and super. One consequence: reassigning clazz after allocation is now sound — type identity and behavior change together, immediately. Previously instanceof(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):

static void resizeHandle_initialize(Class *clazz) {
  ((ControlInterface *) clazz->interface)->captureEvent = resizeHandle_captureEvent;
}

Class *proxy = _initialize(&(const ClassDef) {
  .superclass    = classof(resizeHandle),
  .instanceSize  = classof(resizeHandle)->def.instanceSize,
  .interfaceSize = classof(resizeHandle)->def.interfaceSize,
  .initialize    = resizeHandle_initialize,
});
((Object *) resizeHandle)->clazz = proxy;

The proxy inherits every method (_initialize still memcpys the superclass interface first) and overrides only what its initialize sets. 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: with interfaceOffset gone, a Class can be built from a runtime Class * alone with no compile-time type name; with dispatch reading clazz, the swap actually takes effect. Documented in Documentation/guide.md ("Re-classing an instance") and in the $ doc block.

Verification

  • make clean (0 warnings), make check 26/26 pass with Tests/ unchanged.
  • Examples/Hello and Examples/HelloCpp (C++ consumer of the same headers/macro) build and run.
  • Standalone harness confirmed: zero storage for the member, receiver evaluated once, const receivers, overrides reached through an ancestor static type, inherited methods, and live re-classing.
  • git grep finds no remaining interfaceOffset or old-style member. Xcode/Eclipse templates and Copilot guidance updated.

Caveats

  • Zero-length arrays are a GNU extension — same family as the 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.
  • sizeof of every instance changes, so binaries built against old and new headers must not exchange raw instances. Nothing in these repos does; everything goes through instanceSize.
  • The interface member 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

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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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-length FooInterface *interface[0]; across core types and templates.
  • Removes ClassDef.interfaceOffset and the _alloc loop that previously populated per-instance interface pointers; updates $ to dispatch via classof(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.

Comment thread Sources/Objectively/Class.h
Comment thread Documentation/guide.md Outdated
jdolan and others added 4 commits September 2, 2026 19:25
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>
@jdolan
jdolan merged commit dc73d68 into main Sep 2, 2026
4 checks passed
@jdolan
jdolan deleted the zero-length-interface branch September 2, 2026 23:42
jdolan added a commit to jdolan/ObjectivelyGPU that referenced this pull request Sep 2, 2026
* 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>
jdolan added a commit to jdolan/ObjectivelyMVC that referenced this pull request Sep 2, 2026
)

* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants