From 18ea404c02503cbf448d2a63e84452118e174110 Mon Sep 17 00:00:00 2001 From: Tim Besard Date: Tue, 7 Mar 2023 16:55:54 +0100 Subject: [PATCH] Initial support for blocks. --- src/foundation.jl | 92 +++++++++++++++++++++++++++++++++++++++++++++++ src/syntax.jl | 2 +- test/runtests.jl | 24 +++++++++++++ 3 files changed, 117 insertions(+), 1 deletion(-) diff --git a/src/foundation.jl b/src/foundation.jl index 93329ac..457eaa5 100644 --- a/src/foundation.jl +++ b/src/foundation.jl @@ -366,4 +366,96 @@ function Base.:(==)(a::NSURL, b::NSURL) end +export NSBlock + +# @cenum Block_flags::Cint begin +# BLOCK_DEALLOCATING = 0x0001 +# BLOCK_REFCOUNT_MASK = 0xfffe + +# BLOCK_IS_NOESCAPE = 1 << 23 +# BLOCK_NEEDS_FREE = 1 << 24 +# BLOCK_HAS_COPY_DISPOSE = 1 << 25 +# BLOCK_HAS_CTOR = 1 << 26 +# BLOCK_IS_GLOBAL = 1 << 28 +# BLOCK_HAS_STRET = 1 << 29 +# BLOCK_HAS_SIGNATURE = 1 << 30 +# end + +const _NSConcreteGlobalBlock = cglobal(:_NSConcreteGlobalBlock) +const _NSConcreteStackBlock = cglobal(:_NSConcreteStackBlock) + +@objcwrapper NSBlock <: NSObject + +function Base.copy(block::NSBlock) + @objc [block::id{NSBlock} copy]::id{NSBlock} +end + + +struct JuliaBlockDescriptor + reserved::Culong + size::Culong + copy_helper::Ptr{Cvoid} + dispose_helper::Ptr{Cvoid} +end + +struct JuliaBlock + isa::Ptr{Cvoid} + flags::Cint + reserved::Cint + invoke::Ptr{Cvoid} + descriptor::Ptr{JuliaBlockDescriptor} + + # custom fields + lambda::Function +end + +# JuliaBlock is the concrete version of NSBlock, so make it possible to derive a regular +# Objective-C object by temporarily boxing the structure and copying it to the heap. +function NSBlock(block::JuliaBlock) + block_box = Ref(block) + GC.@preserve block_box begin + block_ptr = Base.unsafe_convert(Ptr{Cvoid}, block_box) + nsblock_ptr = reinterpret(id{NSBlock}, block_ptr) + copy(NSBlock(nsblock_ptr)) + end +end + +# static descriptor for a simple Julia-based block +const julia_block_descriptor = Ref(JuliaBlockDescriptor(0, sizeof(JuliaBlock), 0, C_NULL)) + +function julia_block_trampoline(_block, _self, args...) + block = unsafe_load(_block) + nsblock = NSBlock(reinterpret(id{NSBlock}, _block)) + + # call the user lambda + block.lambda(args...) +end + +macro objcblock(callable, rettyp, argtyps) + quote + cb = @cfunction($julia_block_trampoline, $(esc(rettyp)), + (Ptr{JuliaBlock}, id{Object}, $(esc(argtyps))...)) + GC.@preserve cb begin + # set-up the block data structures + trampoline_ptr = Base.unsafe_convert(Ptr{Cvoid}, cb) + desc_ptr = Base.unsafe_convert(Ptr{Cvoid}, $julia_block_descriptor) + block = JuliaBlock(_NSConcreteStackBlock, 0, 0, trampoline_ptr, desc_ptr, $(esc(callable))) + + # copy the block to the heap and have Objective-C use that object. + # our original block was a plain Julia struct so doesn't need to be released. + # we also don't need to worry about the lifetime of the block object, as the + # only references to Julia data it contains are the descriptor, which is + # statically allocated, and the lambda (Julia code currently isn't GC'd). + # + # XXX: does nobody need to release the NSBlock copy we created? + # also, we will need to keep track of the cfunction lifetime + # if we want to support closures (or make the user responsible + # by requiring the @cfunction output to be passed to the macro, + # or returning an object that links the NSBlock with the CFunction). + NSBlock(block) + end + end +end + + end diff --git a/src/syntax.jl b/src/syntax.jl index ad69515..a796818 100644 --- a/src/syntax.jl +++ b/src/syntax.jl @@ -1,4 +1,4 @@ -export @objc, @objcwrapper, @objcproperties +export @objc, @objcwrapper, @objcproperties, @objcblock # Method Calling diff --git a/test/runtests.jl b/test/runtests.jl index 3c11199..5ee7506 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -37,6 +37,30 @@ end using .Foundation +@testset "NSBlock" begin + function addone(x::T) where T + return x + one(T) + end + + block = Foundation.@objcblock(addone, Cint, (Cint,)) + + # for validation, register our block with a class method + # (no need to use @objcwrapper as we're not constructing an id{BlockWrapper}) + wrapper_class = ObjectiveC.allocclass(:BlockWrapper, Class(:NSObject)) + imp = ccall(:imp_implementationWithBlock, Ptr{Cvoid}, (id{Foundation.NSBlock},), block) + types = (Cint, Object, Selector, Cint) + typestr = ObjectiveC.encodetype(types...) + @assert ccall(:class_addMethod, Bool, + (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cchar}), + wrapper_class, sel"invoke:", imp, typestr) + ObjectiveC.register(wrapper_class) + + # create a wrapper instance and call our block + ptr = @objc [BlockWrapper alloc]::id{Object} + ret = @objc [ptr::id{Object} invoke:41::Cint]::Cint + @test ret == 42 +end + @testset "NSString" begin str = NSString() @test is_kind_of(str, Class("NSString"))