Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NSAutoreleasePool improvements #31

Merged
merged 7 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
coverage:
status:
patch: false
project: false
changes: false
95 changes: 89 additions & 6 deletions src/foundation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export NSObject, retain, release, autorelease, is_kind_of
@autoproperty hash::NSUInteger
@autoproperty description::id{NSString}
@autoproperty debugDescription::id{NSString}

@autoproperty retainCount::NSUInteger
end

function Base.show(io::IO, ::MIME"text/plain", obj::NSObject)
Expand Down Expand Up @@ -409,6 +411,16 @@ export NSAutoreleasePool, @autoreleasepool, drain

@objcwrapper NSAutoreleasePool <: NSObject

"""
NSAutoreleasePool()

Create a new autorelease pool. This is a low-level wrapper around the Objective-C
`NSAutoreleasePool` class, and should be used with care. For example, it does not
automatically get released, or drain the pool on finalization.

For high-level usage, consider using the do-block syntax, or [`@autoreleasepool`](@ref)
instead.
"""
function NSAutoreleasePool()
obj = NSAutoreleasePool(@objc [NSAutoreleasePool alloc]::id{NSAutoreleasePool})
# XXX: this init call itself requires an autoreleasepool to be active...
Expand All @@ -421,7 +433,22 @@ end
drain(pool::NSAutoreleasePool) = @objc [pool::id{NSAutoreleasePool} drain]::Cvoid

# high-level interface to wrap Julia code in an autorelease pool
const NSAutoreleaseLock = ReentrantLock()

"""
NSAutoreleasePool() do
# ...
end

High-level interface to wrap Julia code in an autorelease pool. This is equivalent to
`@autoreleasepool` in Objective-C, and ensures that the pool is drained after the
enclosed code block has finished.

Note that due to technical limitations, this API prevents the current task from migrating
to another thread. In addition, only one autorelease do-block can be active at a time.
To disable these limitations, use the unsafe [`NSUnsafeAutoreleasePool`](@ref) instead.

See also: [`@autoreleasepool`](@ref)
"""
function NSAutoreleasePool(f::Base.Callable)
# we cannot switch between multiple autorelease pools, so ensure only one is ever active.
# XXX: support multiple pools, as long as they run on separate threads?
Expand All @@ -442,12 +469,68 @@ function NSAutoreleasePool(f::Base.Callable)
end
end
end
const NSAutoreleaseLock = ReentrantLock()

function NSUnsafeAutoreleasePool(f::Base.Callable)
pool = NSAutoreleasePool()
try
f()
finally
drain(pool)
end
end


# for compatibility with Objective-C code
macro autoreleasepool(ex)
quote
$NSAutoreleasePool() do
$(esc(ex))
"""
@autoreleasepool [kwargs...] code...
@autoreleasepool [kwargs...] function ... end

High-level interface to wrap Julia code in an autorelease pool. This macro can be used
within a function, or as a function decorator. In both cases, the macro ensures that the
contained code is wrapped in an autorelease pool, and that the pool is drained after the
enclosed code block has finished.

See also: [`NSAutoreleasePool`](@ref)
"""
macro autoreleasepool(ex...)
code = ex[end]
kwargs = ex[1:end-1]

# extract keyword arguments that are handled by this macro
unsafe = false
for kwarg in kwargs
if Meta.isexpr(kwarg, :(=))
key, value = kwarg.args
if key == :unsafe
isa(value, Bool) || throw(ArgumentError("Invalid value for keyword argument `unsafe`: got `$value`, expected literal boolean value"))
unsafe = value
else
error("Invalid keyword argument to @autoreleasepool: $kwarg")
end
else
throw(ArgumentError("Invalid keyword argument to @autoreleasepool: $kwarg"))
end
end
f = unsafe ? NSUnsafeAutoreleasePool : NSAutoreleasePool

if Meta.isexpr(code, :function)
# function definition
sig = code.args[1]
@assert Meta.isexpr(sig, :call)
body = code.args[2]
@assert Meta.isexpr(body, :block)
managed_body = quote
$f() do
$body
end
end
esc(Expr(:function, sig, managed_body))
else
# code block
quote
$f() do
$(esc(code))
end
end
end
end
Expand Down
20 changes: 13 additions & 7 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -108,22 +108,28 @@ using .Foundation
@testset "foundation" begin

@testset "NSAutoReleasePool" begin
# MRC
obj = NSString(@objc [NSString new]::id{NSString})
@objc [obj::id{NSString} release]::id{NSString}
# a function that creates an `autorelease`d object (by calling `arrayWithObjects`)
function trigger_autorelease()
str1 = NSString("Hello")
str2 = NSString("World")
arr1 = [str1, str2]
NSArray([str1, str2])
end

# low-level API
let pool=NSAutoreleasePool()
obj = NSString(@objc [NSString new]::id{NSString})
autorelease(obj)
trigger_autorelease()
drain(pool)
end

# high-level API
@autoreleasepool begin
# calling the `string` constructor means we don't need `autorelease`
@objc [NSString string]::id{NSString}
trigger_autorelease()
end
@autoreleasepool function foo()
trigger_autorelease()
end
foo()
end

# run the remainder of the tests in an autorelease pool to avoid leaking objects
Expand Down
Loading