JuliaInterop / ObjectiveC.jl

Objective-C embedded in Julia
Other
39 stars 10 forks source link

Reconsider type model #13

Open maleadt opened 1 year ago

maleadt commented 1 year ago

Something that may be worth discussing: in a8c859f I decided to 'split' the Julia class definitions we generate with @objcwrapper (a helper macro to generate object classes) into two: an abstract class to implement the hierarchy, and a concrete one that contains the object pointer. The motivation here is that Objective-C has multilevel inheritance, e.g., MTLEvent <: NSObject is a perfectly valid object, but there's also MTLSharedEvent <: MTLEvent which augments MTLEvent. We can't express that in Julia, lacking concrete inheritance, and the traditional alternatives (composition instead of inheritance) break dispatch.

After a8c859f, we have MTLEvent be an abstract class so that MTLSharedEvent can inherit from it, and objects will be put in the MTLEventInstance or MTLSharedEventInstance types, depending on the requested type. To make this all invisible, I generate a pseudo constructor for the abstract classes (so calling MTLEvent(ptr::id) will give an MTLEventInstance). Case in point, the change did not break the WIP wrappers I have over in JuliaGPU/Metal.jl#117, yet it did allow me to remove the 'fake' MTLAbstractEvent class I had introduced there. So this looks like a potentially interesting solution for supporting Objective-C's object model (although I may still be missing things, as I'm not really familiar with the language yet and am just designing things in function of the Metal APIs).

I also considered making everything an Object{T}, but that would make methods ugly (e.g., foo(obj::Object{<:MTLEvent}) instead of now simply foo(obj::MTLEvent)).

I faced this challenge in Gtk.jl and ended up doing something similar to this, with GObject and GObjectLeaf (making the interface the easier type, similar to Ref vs. Base.RefValue). I was never quite satisfied with this though, as it felt awkward, led to excess specialization, was a bit hard to extend in disjoint packages, and never quite mapped properly between the type systems (since calling interface methods requires an explicit type cast in the Julia Gtk.jl wrapper). If I was to revisit that decision, I might have gone with a runtime-traits-based approach instead, perhaps with optional mixins.

mutable struct GObject{Mixin}
    classid::Int # or encoded in gptr
    gptr::Ptr{Cvoid}
    mixins::Mixin
end

Then at runtime, it would be sufficient to do the reflection calls needed to decide if a particular interface was applicable, and perhaps also to provide a custom dispatch table hook for places where it is required. This would have let it provide all of the features (multiple inheritance, concrete inheritance, interfaces with inheritance) that are not viable normally.

Secondly, with the current approach, I think I should have at least implemented many convert-trait methods, and coded them up to be visible to reflection:

abstract type AnyInterface; end
abstract type AnyObject; end
struct Interface{T} <: AnyInterface; obj::T; end
inherits_from(::Type, ::Type) = false
inherits_from(::Type{<:Interface}, ::Type{<:Abstract}) = true
convert(::Type{I}, obj::AnyObject) where {I<:AnyInterface} = I(obj)

then when I auto-generated interface calls such as label, I could also auto-generate dispatch methods that try to select the best matching ccall:

struct HasLabel{T} <: AnyInterface; obj::T; end
label(obj::HasLabel) = ccall
function label(obj)
    inherits_from(HasLabel, obj) && return label(HasLabel(obj))
    throw(InterfaceMissingError(label, obj))
end

Originally posted by @vtjnash in https://github.com/JuliaInterop/ObjectiveC.jl/issues/11#issuecomment-1458284161