Conversation
Resolves the boxing/inference cost of the old abstract-type-with-Instance-suffix split (issue #13) by emitting `@objcwrapper`'d classes as concrete structs that all subtype the sole abstract `Object`. The ObjC class hierarchy is recorded separately as a runtime trait via `objc_parent` / `inherits_from`, walked at compile time so the const-folded dispatch costs nothing. For methods polymorphic over a parent class and its wrapped subclasses, a new `@objcdispatch f(::KindOf{T}, …)` macro emits the canonical body on a `KindOf{T}` covariant wrapper (modeled on ObjC's `__kindof T *`) plus a `(::Object, …)` forwarder that routes via `inherits_from`. `KindOf{T}` is itself a bitstype wrapper of `id{T}`, so the indirection compiles to a register move. See the README's new "Type model" section for the three method-writing patterns and when to reach for each. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`KindOf{T}` is no longer a wrapper struct — it's a marker that `@objcdispatch`
substitutes at macro-expansion time with `Union{T, descendants(T)...}`, where
descendants are enumerated by walking the `objc_parent` method table. The
result is a regular Julia method on a concrete Union, dispatched natively, with
no forwarder, no `inherits_from` runtime check, and no `(::Object, ...)` method.
Side effects:
- `typeof(arg)` inside an `@objcdispatch` body is the concrete subclass. The
previous "manual `::Object` + `inherits_from` guard" pattern (needed when
the body had to recover the caller's type) folds back into `@objcdispatch`.
`Base.copy(::MPSKernel)` and `Base.close(::MTLCommandEncoder)` in the Metal
port are now plain `@objcdispatch`'s.
- Two `@objcdispatch` methods on different parents no longer fight over a
shared `(::Object, ...)` forwarder. Their Unions are disjoint, so they
co-exist as distinct methods. The manual ndarray.jl branching for
Unary 2-arg vs Binary 2-arg `encode!` is unnecessary.
Contract: `@objcwrapper Sub <: Parent` must be declared before any
`@objcdispatch` on `KindOf{Parent}` or an ancestor — otherwise the Union is
frozen without `Sub`. `@objcwrapper` walks the `objcdispatch_sites` registry
and emits a warning pointing at affected call sites when the contract is
violated, instructing the user to redeclare the method.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously only the first KindOf{T} argument was rewritten; any further
slots stayed as the literal empty marker `KindOf{T}` struct, producing a
method that could never be called. Substitute every slot independently
and register a call site under every distinct parent type.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`Base.propertynames` was only emitted by `@objcproperties`, so a wrapper
declared without one — common for leaf classes that add no Julia-side
properties of their own — fell back to `fieldnames` and returned `(:ptr,)`.
The runtime accessor chain handled inherited properties correctly via
`objc_getproperty`, but `propertynames` did not surface them.
Emit `Base.propertynames` from `@objcwrapper` itself, and make the
default `objc_propertynames(::Type{<:Object})` walk `objc_parent` instead
of returning an empty list. The per-class method emitted by
`@objcproperties` continues to override this fallback when present.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Redeclaring `@objcdispatch` after a late `@objcwrapper` emits a new method with a wider Union but the narrower method stays in the table — the signatures genuinely differ, so Julia keeps both. The wider method only wins for the new subclass; the original is still selected for every other type. Recommend only the safe option (move the wrapper above the methods). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`@objcwrapper Foo` (no explicit parent) emitted unqualified `Object` in the `objc_parent` method and `warn_late_subclass` call, so a caller that imported only the macro — but not `Object` — got a `UndefVarError` at expansion time. The struct's `<: Object` declaration was already qualified; this brings the rest of the emitted code in line. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When `T` is abstract (e.g. `Object` itself), every wrapped class is
already `<: T` in Julia's type system, so `Union{T, descendants...}`
simplifies to `T`. The result is that `@objcdispatch f(x::KindOf{Object})`
behaves identically to `f(x::Object)`. Recognise this up front:
- skip the `objc_subtree` method-table walk (the Union would collapse
anyway),
- skip registering the call site, so subsequent `@objcwrapper`s don't
warn that the (non-existent) Union is stale.
`::Object` remains the idiomatic way to dispatch over any wrapper; this
just makes `KindOf{Object}` an acceptable, equivalent spelling rather
than one that drags a warning along every new subclass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without `@inline`, the per-class `objc_getproperty`/`objc_setproperty!` methods emitted by `@objcproperties` block Julia's interprocedural constant propagation: a literal `obj.length` did not flow the `:length` symbol through the forwarder chain, so the `if field === :length` cascade could not be folded and inference fell back to the Union of every property type in the ancestor chain. This was a regression vs. the pre-rework `Base.getproperty(::T, ::Symbol)` that emitted the cascade directly on `Base.getproperty` and benefited from const-prop without annotation. The rework moved the cascade behind an extra forwarder (the `objc_getproperty(T, obj, field)` indirection that walks the trait-based parent chain), and the missing `@inline` defeated the propagation Julia would otherwise have done. Found via the profile in #13 (cross-ref to JuliaGPU/Metal.jl#621), which showed the wide `Union{NSStringInstance, MTLBufferInstance, ...}` body type on `unsafe_convert`/`getproperty` call sites along the kernel-launch path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closed-world `@objcdispatch f(::KindOf{T})` freezes the dispatch Union
at macro-expansion time, so a subclass `@objcwrapper`'d later (e.g. in
a downstream package) doesn't flow through. That's the right model for
methods owned by a single module, but the wrong one for foundation
primitives like `retain`/`release`/`==`/`is_kind_of` that should remain
available to every wrapper declared anywhere.
`@objcdispatch open=true f(::KindOf{T})` substitutes `KindOf{T}` with
the abstract apex `Object` and prepends a runtime guard
`inherits_from(typeof(arg), T) || throw(MethodError(f, args))` to the
method body. The dispatch matches any wrapper, the guard rejects
non-`T` arguments with a clear `MethodError`, and the call site is
not registered — subsequent `@objcwrapper`s never trigger a
late-subclass warning for it. `inherits_from` is `@inline`d, so for
the typical call site where the concrete arg type is static, the
guard folds at compile time and the open variant compiles to the same
code as the closed one.
Convert the foundation NSObject methods (`release`, `autorelease`,
`retain`, `is_kind_of`, `==`, `show(::MIME"text/plain", …)`) over to
`open=true`. They previously took `::Object` directly and leaned on
`convert(id{NSObject}, …)` failing for non-NSObject arguments — the
new spelling documents the intent and surfaces a `MethodError` against
the function instead of an `ArgumentError` against the id conversion.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Member
Author
|
JuliaGPU/Metal.jl#776 seems to be working fine, so let's try this out. |
Contributor
|
Small nit but but the “ |
Member
Author
|
Yeah that's LLMs writing annoying comments based on interim state... Thanks for reading over. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Today,
@objcwrapper Foo <: Baremits an abstractFooplus a concreteFooInstance. Anything stored as aFoo(containers, struct fields, inferred return types) carries an abstract type, which boxes inVector{Foo}, breaks inference through property chains, and triggers dynamic dispatch in profiles (cf. vchuravy's profile).The reason we do this is because ObjC's class hierarchy is concrete inheritance with state at every level. For example,
NSMutableStringis a concrete class with fields and methods, andNSStringis also a concrete class with fields and methods thatNSMutableStringextends. Julia's type system forbids this: only abstract types can have subtypes, only leaves can be concrete. There is no Julia equivalent to "a concrete struct that inherits from another concrete struct."With this PR, each
@objcwrapper Foo <: Barnow emits two parallel definitions:Crucially, the inheritance stays flat: every wrapper is
<: Objectdirectly so thatVector{NSString}is bits-packed and inference sees fixed types through property access.This of course breaks dispatch on "non-leaf" classes, for which we introduce a parallel Kind lattice used by
@objcmethodmethods:The macro
KindOf{T}lowers to trait dispatch onType{<:classkind(T)}plus a method that forwards from untypedObjectinputs. Multiple sites on the same function compose naturally -- Julia's dispatch picks the most specific Kind at the call site -- and downstream wrappers automatically participate by virtue ofSubKind <: ParentKind. When the static type is known at the call site (the common case), the entry-then-body chain folds at compile time, so the trait dispatch is often zero-cost.Closes #13