diff --git a/Project.toml b/Project.toml index dceaf65..30e4959 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ObjectiveC" uuid = "e86c9b32-1129-44ac-8ea0-90d5bb39ded9" -version = "3.3.0" +version = "3.4.0" [deps] CEnum = "fa961155-64e5-5f13-b03f-caf6b980ea82" diff --git a/src/ObjectiveC.jl b/src/ObjectiveC.jl index dff473a..9502845 100644 --- a/src/ObjectiveC.jl +++ b/src/ObjectiveC.jl @@ -29,8 +29,12 @@ const tracing = @load_preference("tracing", false)::Bool include("primitives.jl") include("methods.jl") +# Get macOS and Darwin version +include("version.jl") + # Calls & Properties include("abi.jl") +include("availability.jl") include("syntax.jl") # API wrappers diff --git a/src/availability.jl b/src/availability.jl new file mode 100644 index 0000000..32f8094 --- /dev/null +++ b/src/availability.jl @@ -0,0 +1,103 @@ +export PlatformAvailability, UnavailableError + +# Each platform tuple has a symbol representing the constructor, a pretty name for errors, +# and a symbol of the function used to check the version for that platform +const SUPPORTED_PLATFORMS = [(:macos, "macOS", :macos_version), (:darwin, "Darwin", :darwin_version)] + +# Based off of Clang's `CXPlatformAvailability` +""" + PlatformAvailability(platform::Symbol, introduced[, deprecated, obsoleted, unavailable]) + PlatformAvailability(platform::Symbol, ; [introduced, deprecated, obsoleted, unavailable]) + +Creates a `PlatformAvailability{platform}` object representing an availability statement for Objective-C wrappers. + +The currently supported values for `platform` are: +- `:macos`: for macOS version availability +- `:darwin`: for Darwin kernel availability +""" +struct PlatformAvailability{P} + introduced::Union{Nothing, VersionNumber} + deprecated::Union{Nothing, VersionNumber} + obsoleted::Union{Nothing, VersionNumber} + unavailable::Bool + + function PlatformAvailability(platform::Symbol, introduced, deprecated = nothing, obsoleted = nothing, unavailable = false) + platform in first.(SUPPORTED_PLATFORMS) || throw(ArgumentError(lazy"`:$platform` is not a supported platform for `PlatformAvailability`, see `?PlatformAvailability` for more information.")) + return new{platform}(introduced, deprecated, obsoleted, unavailable) + end +end +PlatformAvailability(platform; introduced = nothing, deprecated = nothing, obsoleted = nothing, unavailable = false) = + PlatformAvailability(platform, introduced, deprecated, obsoleted, unavailable) + +function is_unavailable(f::Function, avail::PlatformAvailability) + return avail.unavailable || + (!isnothing(avail.obsoleted) && f() >= avail.obsoleted) || + (!isnothing(avail.introduced) && f() < avail.introduced) +end +is_unavailable(avails::Vector{<:PlatformAvailability}) = any(is_unavailable.(avails)) + +""" + UnavailableError(symbol::Symbol, minver::VersionNumber) + +Attempt to contruct an Objective-C object or property that is +not available in the current macOS version. +""" +struct UnavailableError <: Exception + symbol::Symbol + msg::String +end +function UnavailableError(f::Function, symbol::Symbol, platform::String, avail::PlatformAvailability) + msg = if avail.unavailable + "is not available on $platform" + elseif !isnothing(avail.obsoleted) && f() >= avail.obsoleted + "is obsolete since $platform v$(avail.obsoleted)" + elseif !isnothing(avail.introduced) && f() < avail.introduced + "was introduced on $platform v$(avail.introduced)" + else + "does not seem to be unavailable. Please file an issue at www.github.com/JuliaInterop/ObjectiveC.jl with the source of the offending Objective-C code." + end + return UnavailableError(symbol, msg) +end +function UnavailableError(symbol::Symbol, avails::Vector{<:PlatformAvailability}) + firsterror = findfirst(is_unavailable, avails) + return UnavailableError(symbol, avails[firsterror]) +end + +function Base.showerror(io::IO, e::UnavailableError) + print(io, "UnavailableError: `", e.symbol, "` ", e.msg) + return +end + +# Platform-specific definitions +for (name, pretty_name, version_function) in SUPPORTED_PLATFORMS + quotname = Meta.quot(name) + @eval begin + is_unavailable(avail::PlatformAvailability{$quotname}) = is_unavailable($version_function, avail) + UnavailableError(symbol::Symbol, avail::PlatformAvailability{$quotname}) = UnavailableError($version_function, symbol, $pretty_name, avail) + end +end + +function get_avail_exprs(mod, expr) + transform_avail_exprs!(expr) + avail = Base.eval(mod, expr) + + return avail +end + +function transform_avail_exprs!(expr) + if Meta.isexpr(expr, :vect) + for availexpr in expr.args + transform_avail_expr!(availexpr) + end + else + transform_avail_expr!(expr) + end + return expr +end +function transform_avail_expr!(expr) + @assert Meta.isexpr(expr, :call) "`availability` keyword argument must be a valid `PlatformAvailability` constructor or vector." + expr.args[1] = Meta.quot(expr.args[1]) + insert!(expr.args, 1, :PlatformAvailability) + return expr +end + diff --git a/src/syntax.jl b/src/syntax.jl index c022ffa..d447fed 100644 --- a/src/syntax.jl +++ b/src/syntax.jl @@ -250,11 +250,11 @@ function instance_message(instance, typ, msg, rettyp, argtyps, argvals) end end +# TODO: support availability macro objc(ex) objcm(__module__, ex) end - # Wrapper Classes wrappererror(msg) = error("""ObjectiveC wrapper: $msg @@ -278,6 +278,7 @@ keyword arguments: * `immutable`: if `true` (default), define the instance class as an immutable. Should be disabled when you want to use finalizers. + * `availability`: A `PlatformAvailability` object that represents the availability of the object. * `comparison`: if `true` (default `false`), define `==` and `hash` methods for the wrapper class. This should not be necessary when using an immutable struct, in which case the default `==` and `hash` methods are sufficient. @@ -289,6 +290,7 @@ macro objcwrapper(ex...) # parse kwargs comparison = nothing immutable = nothing + availability = nothing for kw in kwargs if kw isa Expr && kw.head == :(=) kw, value = kw.args @@ -298,6 +300,8 @@ macro objcwrapper(ex...) elseif kw == :immutable value isa Bool || wrappererror("immutable keyword argument must be a literal boolean") immutable = value + elseif kw == :availability + availability = get_avail_exprs(__module__, value) else wrappererror("unrecognized keyword argument: $kw") end @@ -307,6 +311,7 @@ macro objcwrapper(ex...) end immutable = something(immutable, true) comparison = something(comparison, !immutable) + availability = something(availability, PlatformAvailability(:macos, v"0")) # parse class definition if Meta.isexpr(def, :(<:)) @@ -347,6 +352,10 @@ macro objcwrapper(ex...) # add a pseudo constructor to the abstract type that also checks for nil pointers. function $name(ptr::id) + @static if !Sys.isapple() || ObjectiveC.is_unavailable($availability) + throw($UnavailableError(Symbol($name), $availability)) + end + ptr == nil && throw(UndefRefError()) $instance(ptr) end @@ -391,7 +400,7 @@ propertyerror(s::String) = error("""Objective-C property declaration: $s. """ @objcproperties ObjCType begin - @autoproperty myProperty::ObjCType [type=JuliaType] [setter=setMyProperty] + @autoproperty myProperty::ObjCType [type=JuliaType] [setter=setMyProperty] [getter=getMyProperty] [availability::PlatformAvailability] @getproperty myProperty function(obj) ... @@ -416,7 +425,8 @@ contains a series of property declarations: - `setter`: specifies the name of the Objective-C setter method. Without this, no `setproperty!` definition will be generated. - `getter`: specifies the name of the Objective-C getter method. Without this, the - getter method is assumed to be identical to the property + getter method is assumed to be identical to the property. + - `availability`: A `PlatformAvailability` object that represents the availability of the property. - `@getproperty myProperty function(obj) ... end`: define a custom getter for the property. The function should take a single argument `obj`, which is the object that the property is being accessed on. The function should return the property value. @@ -492,6 +502,12 @@ macro objcproperties(typ, ex) # caller's module and decide on the appropriate ABI. that necessitates use of # :hygienic-scope to handle the mix of esc/hygienic code. + availability = nothing + if haskey(kwargs, :availability) + availability = get_avail_exprs(__module__, kwargs[:availability]) + end + availability = something(availability, PlatformAvailability(:macos, v"0")) + getterproperty = if haskey(kwargs, :getter) kwargs[:getter] else @@ -499,6 +515,9 @@ macro objcproperties(typ, ex) end getproperty_ex = objcm(__module__, :([object::id{$(esc(typ))} $getterproperty]::$srcTyp)) getproperty_ex = quote + @static if !Sys.isapple() || ObjectiveC.is_unavailable($availability) + throw($UnavailableError(Symbol($(esc(typ)), ".", field), $availability)) + end value = $(Expr(:var"hygienic-scope", getproperty_ex, @__MODULE__, __source__)) end diff --git a/src/version.jl b/src/version.jl new file mode 100644 index 0000000..16515d7 --- /dev/null +++ b/src/version.jl @@ -0,0 +1,78 @@ +# version and support queries + +@noinline function _syscall_version(name) + size = Ref{Csize_t}() + err = @ccall sysctlbyname( + name::Cstring, C_NULL::Ptr{Cvoid}, size::Ptr{Csize_t}, + C_NULL::Ptr{Cvoid}, 0::Csize_t + )::Cint + Base.systemerror("sysctlbyname", err != 0) + + osrelease = Vector{UInt8}(undef, size[]) + err = @ccall sysctlbyname( + name::Cstring, osrelease::Ptr{Cvoid}, size::Ptr{Csize_t}, + C_NULL::Ptr{Cvoid}, 0::Csize_t + )::Cint + Base.systemerror("sysctlbyname", err != 0) + + verstr = view(String(osrelease), 1:(size[] - 1)) + return parse(VersionNumber, verstr) +end + +@static if isdefined(Base, :OncePerProcess) # VERSION >= v"1.12.0-DEV.1421" + const darwin_version = OncePerProcess{VersionNumber}() do + _syscall_version("kern.osrelease") + end + const macos_version = OncePerProcess{VersionNumber}() do + _syscall_version("kern.osproductversion") + end +else + const _darwin_version = Ref{VersionNumber}() + function darwin_version() + if !isassigned(_darwin_version) + _darwin_version[] = _syscall_version("kern.osrelease") + end + _darwin_version[] + end + + const _macos_version = Ref{VersionNumber}() + function macos_version() + if !isassigned(_macos_version) + _macos_version[] = _syscall_version("kern.osproductversion") + end + _macos_version[] + end +end + +@doc """ + ObjectiveC.darwin_version()::VersionNumber + +Returns the host Darwin kernel version. + +See also [`ObjectiveC.macos_version`](@ref). +""" darwin_version + +@doc """ + ObjectiveC.macos_version()::VersionNumber + +Returns the host macOS version. + +See also [`ObjectiveC.darwin_version`](@ref). +""" macos_version + +""" + Metal.is_macos([ver::VersionNumber]) -> Bool + +Returns whether the OS is macOS with version `ver` or newer. + +See also [`Metal.macos_version`](@ref). +""" +function is_macos(ver = nothing) + return if !Sys.isapple() + false + elseif ver === nothing + true + else + macos_version() >= ver + end +end diff --git a/test/runtests.jl b/test/runtests.jl index d9da239..74f822d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,98 @@ using ObjectiveC using Test +@testset "version" begin + @test ObjectiveC.darwin_version() isa VersionNumber + @test ObjectiveC.macos_version() isa VersionNumber + @test ObjectiveC.is_macos(ObjectiveC.macos_version()) +end + +# Availability +@objcwrapper availability = macos(v"1000") TestWrapperNoIntro1 <: Object +@objcwrapper availability = macos(introduced = v"1000") TestWrapperNoIntro2 <: Object +@objcwrapper availability = macos(deprecated = v"1", obsoleted = v"2.3.4") TestWrapperObsolete <: Object +@objcwrapper availability = macos(introduced = v"1000", unavailable = true) TestWrapperUnavailable <: Object +@objcwrapper availability = macos(v"0") TestPropAvail <: Object +@objcproperties TestPropAvail begin + @autoproperty length::Culong + @autoproperty UTF8String::Ptr{Cchar} availability = macos(v"0") + @autoproperty NoIntro1Property::Cint availability = macos(v"1000") + @autoproperty NoIntro2Property::Cint availability = macos(introduced = v"1000") + @autoproperty ObsoleteProperty::Cint availability = macos(deprecated = v"1", obsoleted = v"2.3") + @autoproperty UnavailableProperty::Cint availability = macos(introduced = v"1000", unavailable = true) +end +@objcwrapper availability = [macos(v"1000")] TestVectUnavail <: Object +@objcwrapper availability = [macos(v"1000"), darwin(v"0")] TestVectMultiple1 <: Object +@objcwrapper availability = [macos(v"0"), darwin(v"1000")] TestVectMultiple2 <: Object +@objcwrapper availability = [macos(v"0"), darwin(v"0")] TestVectMultiple3 <: Object +@objcwrapper availability = [macos(v"0")] TestVectAvail <: Object +@objcproperties TestVectAvail begin + @autoproperty length::Culong + @autoproperty UTF8String::Ptr{Cchar} availability = [macos(v"0")] + @autoproperty VectUnavailableProperty::Cint availability = [darwin(introduced = v"1000")] +end +@testset "availability" begin + # wrapper + let # not yet introduced arg version + fakeidwrap = id{TestWrapperNoIntro1}(1) + @test_throws "UnavailableError: `TestWrapperNoIntro1` was introduced on macOS v1000.0.0" TestWrapperNoIntro1(fakeidwrap) + end + let # not yet introduced kwarg version + fakeidwrap = id{TestWrapperNoIntro2}(1) + @test_throws "UnavailableError: `TestWrapperNoIntro2` was introduced on macOS v1000.0.0" TestWrapperNoIntro2(fakeidwrap) + end + let # obsolete + fakeidwrap = id{TestWrapperObsolete}(1) + @test_throws "UnavailableError: `TestWrapperObsolete` is obsolete since macOS v2.3.4" TestWrapperObsolete(fakeidwrap) + end + let # unavailable + fakeidwrap = id{TestWrapperUnavailable}(1) + @test_throws "UnavailableError: `TestWrapperUnavailable` is not available on macOS" TestWrapperUnavailable(fakeidwrap) + end + let # not yet introduced in vector + fakeidwrap = id{TestVectUnavail}(1) + @test_throws "UnavailableError: `TestVectUnavail` was introduced on macOS v1000.0.0" TestVectUnavail(fakeidwrap) + end + let # not yet introduced in vector for multiple + fakeidwrap = id{TestVectMultiple1}(1) + @test_throws "UnavailableError: `TestVectMultiple1` was introduced on macOS v1000.0.0" TestVectMultiple1(fakeidwrap) + end + let # not yet introduced in vector for multiple + fakeidwrap = id{TestVectMultiple2}(1) + @test_throws "UnavailableError: `TestVectMultiple2` was introduced on Darwin v1000.0.0" TestVectMultiple2(fakeidwrap) + end + let # Make sure it does not error + fakeidwrap = id{TestVectMultiple3}(1) + @test TestVectMultiple3(fakeidwrap) isa TestVectMultiple3 + end + + # property + str1 = "foo" + prop = TestPropAvail(@objc [NSString stringWithUTF8String:str1::Ptr{UInt8}]::id{TestPropAvail}) + + @test :length in propertynames(prop) + @test :UTF8String in propertynames(prop) + @test :NoIntro1Property in propertynames(prop) + @test :NoIntro2Property in propertynames(prop) + @test :ObsoleteProperty in propertynames(prop) + @test :UnavailableProperty in propertynames(prop) + + @test prop.length == length(str1) + @test unsafe_string(prop.UTF8String) == str1 + @test_throws "UnavailableError: `TestPropAvail.NoIntro1Property` was introduced on macOS v1000.0.0" prop.NoIntro1Property + @test_throws "UnavailableError: `TestPropAvail.NoIntro2Property` was introduced on macOS v1000.0.0" prop.NoIntro2Property + @test_throws "UnavailableError: `TestPropAvail.ObsoleteProperty` is obsolete since macOS v2.3.0" prop.ObsoleteProperty + @test_throws "UnavailableError: `TestPropAvail.UnavailableProperty` is not available on macOS" prop.UnavailableProperty + + vectprop = TestVectAvail(@objc [NSString stringWithUTF8String:str1::Ptr{UInt8}]::id{TestVectAvail}) + @test_throws "UnavailableError: `TestVectAvail.VectUnavailableProperty` was introduced on Darwin v1000.0.0" vectprop.VectUnavailableProperty + + @test_throws "`:templeos` is not a supported platform for `PlatformAvailability`" macroexpand(@__MODULE__, :(@objcwrapper availability = templeos(v"1000") TestBadAvail2 <: Object)) + @test_throws "`:templeos` is not a supported platform for `PlatformAvailability`" macroexpand(@__MODULE__, :(@objcwrapper availability = [templeos(v"1000")] TestBadAvail3 <: Object)) + @test_throws "`availability` keyword argument must be a valid `PlatformAvailability`" macroexpand(@__MODULE__, :(@objcwrapper availability = [6] TestBadAvail4 <: Object)) + @test_throws "`availability` keyword argument must be a valid `PlatformAvailability`" macroexpand(@__MODULE__, :(@objcwrapper availability = 6 TestBadAvail5 <: Object)) +end + @testset "@objc macro" begin # class methods @objc [NSString new]::id{Object}