Skip to content

Commit

Permalink
Dispatch & Timeout (#9)
Browse files Browse the repository at this point in the history
* dispatch/timeout on Linux.

* Windows.

* Windows tests.

* Apple.

* Finish.

* Debug macOS.

* Use `NSValue` as user info.

* Finish.
  • Loading branch information
sunoru authored Dec 7, 2022
1 parent e9ed3a1 commit 54cdb9b
Show file tree
Hide file tree
Showing 10 changed files with 293 additions and 103 deletions.
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ version = "1.0.0"

[deps]
Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
FunctionWrappers = "069b7b12-0de2-55c6-9aab-29f3d0a68a2e"
JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1"
Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
Expand All @@ -13,6 +14,7 @@ SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce"

[compat]
Downloads = "1.6"
FunctionWrappers = "1.1"
JSON3 = "1.12"
Reexport = "1.2"
julia = "1.8"
48 changes: 33 additions & 15 deletions src/API.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ export terminate,
eval!,
bind_raw,
unbind,
return_raw
return_raw,
set_timeout,
clear_timeout

using JSON3: JSON3

Expand Down Expand Up @@ -77,17 +79,15 @@ function Base.run(w::AbstractWebview)
end

"""
dispatch(f::Function, w::Webview, [arg])
dispatch(f::Function, w::Webview)
Posts a function to be executed on the main thread. You normally do not need
to call this function, unless you want to tweak the native window.
The function `f` will be called with two arguments: the webview and the `arg`.
The function `f` should be callable without arguments.
"""
function dispatch(f::Function, w::AbstractWebview, arg=nothing)
dispatch(w.platform) do
f(w, arg)
end
function dispatch(f::Function, w::AbstractWebview)
dispatch(f, w.platform)
nothing
end

Expand Down Expand Up @@ -122,8 +122,8 @@ Sets the native window size.
"""
function Base.resize!(
w::AbstractWebview,
size::Tuple{Integer, Integer};
hint::Union{WindowSizeHint, Nothing}=WEBVIEW_HINT_NONE
size::Tuple{Integer,Integer};
hint::Union{WindowSizeHint,Nothing}=WEBVIEW_HINT_NONE
)
resize!(w.platform, size, hint=hint)
w
Expand Down Expand Up @@ -161,15 +161,15 @@ is ignored. Use `bind` if you want to receive notifications about the results of
@forward eval!(w, js::AbstractString)

"""
bind_raw(f::Function, w::Webview, name::AbstractString, [arg])
bind_raw(f::Function, w::Webview, name::AbstractString)
Binds a callback so that it will appear in the webview with the given name
as a global async JavaScript function. Callback receives a seq and req value.
The seq parameter is an identifier for using `Webviews.return_raw` to
return a value while the req parameter is a string of an JSON array representing
the arguments passed from the JavaScript function call.
The callback function must has the method `f(seq::String, req::String, [arg::Any])`.
The callback function must has the method `f(seq::String, req::String)`.
"""
function bind_raw end

Expand All @@ -185,7 +185,7 @@ return value to the webview.
The callback function must handle a `Tuple` as its argument.
"""
function Base.bind(f::Function, w::AbstractWebview, name::AbstractString)
bind_raw(w, name) do seq, args, _
bind_raw(w, name) do seq, args
try
result = f(Tuple(args))
_return(w, seq, true, result)
Expand All @@ -209,11 +209,11 @@ Allows to return a value from the native binding. Original request pointer
must be provided to help internal RPC engine match requests with responses.
"""
function return_raw(w::AbstractWebview, seq::Int, success::Bool, result_or_err::AbstractString)
dispatch(w, nothing) do w, _
dispatch(w) do
if success
eval!(w, "window._rpc[$seq].resolve($result_or_err); delete window._rpc[$seq]");
eval!(w, "window._rpc[$seq].resolve($result_or_err); delete window._rpc[$seq]")
else
eval!(w, "window._rpc[$seq].reject($result_or_err); delete window._rpc[$seq]");
eval!(w, "window._rpc[$seq].reject($result_or_err); delete window._rpc[$seq]")
end
end
end
Expand All @@ -232,6 +232,24 @@ function _return(w::AbstractWebview, seq::Int, success::Bool, result)
return_raw(w, seq, success, s)
end

"""
set_timeout(f::Function, w::Webview, interval::Real; [repeat::Bool=false])
Sets a function to be called after the given interval in webview's event loop.
If `repeat` is `true`, `f` will be called repeatedly.
The function `f` should be callable without arguments.
This function returns a `timer_id::Ptr{Cvoid}` which can be used in `clear_timeout(webview, timer_id)`.
"""
set_timeout(f::Function, w::AbstractWebview, interval::Real; repeat::Bool=false) =
set_timeout(f, w.platform, interval; repeat)

"""
clear_timeout(w::Webview, timer_id::Ptr{Cvoid})
Clears a previously set timeout.
"""
@forward clear_timeout(w, timer_id::Ptr{Cvoid})

const run = Base.run
const size = Base.size
const resize! = Base.resize!
Expand Down
63 changes: 57 additions & 6 deletions src/Utils.jl
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
module Utils

using FunctionWrappers: FunctionWrapper
using JSON3: JSON3

using ..API

const MessageCallback = FunctionWrapper{Nothing,Tuple{Int,Vector{Any}}}

# JuliaLang/julia #269
mutable struct _DispatchCallback{T}
const func::FunctionWrapper{Nothing,Tuple{}}
const ch::T
end

mutable struct CallbackHandler
const callbacks::Dict{String,Base.RefValue{Tuple{Function,Any}}}
CallbackHandler() = new(Dict())
const callbacks::Dict{String,MessageCallback}
# func -> seq
const dispatched::Dict{_DispatchCallback{CallbackHandler}, UInt64}

CallbackHandler() = new(Dict(), Dict())
end
const DispatchCallback = _DispatchCallback{CallbackHandler}

function on_message(ch::CallbackHandler, s::Ptr{Cchar})
try
Expand All @@ -16,17 +29,20 @@ function on_message(ch::CallbackHandler, s::Ptr{Cchar})
haskey(ch.callbacks, name) || return
seq = msg.id
args = msg.params
f, arg = ch.callbacks[name][]
f(seq, copy(args), arg)
f = ch.callbacks[name]
f(seq, copy(args))
catch e
@debug "Error occured while handling message: $e"
end
nothing
end

function API.bind_raw(f::Function, ch::CallbackHandler, name::AbstractString, arg=nothing)
function API.bind_raw(f::Function, ch::CallbackHandler, name::AbstractString)
haskey(ch.callbacks, name) && return
ch.callbacks[name] = Ref{Tuple{Function,Any}}((f, arg))
ch.callbacks[name] = MessageCallback() do seq, args
f(seq, args)
nothing
end
nothing
end

Expand All @@ -35,4 +51,39 @@ function API.unbind(ch::CallbackHandler, name::AbstractString)
nothing
end

# Returns a pointer to the function wrapper as the ID.
function setup_dispatch(f::Function, ch::CallbackHandler)
func = FunctionWrapper{Nothing,Tuple{}}() do
try
f()
catch e
@debug "Error occured while dispatching: $e"
end
nothing
end
dcb = DispatchCallback(func, ch)
ch.dispatched[dcb] = 0
ptr = pointer_from_objref(dcb)
end

function set_dispatch_id(ptr::Ptr{Cvoid}, id::Integer)
dcb = unsafe_pointer_to_objref(ptr)::DispatchCallback
dcb.ch.dispatched[dcb] = UInt64(id)
nothing
end

function call_dispatch(ptr::Ptr{Cvoid})
dcb = unsafe_pointer_to_objref(ptr)::DispatchCallback
dcb.func()
end

function clear_dispatch(ptr::Ptr{Cvoid})
dcb = unsafe_pointer_to_objref(ptr)::DispatchCallback
dispatched = dcb.ch.dispatched
haskey(dispatched, dcb) || return nothing
id = dispatched[dcb]
delete!(dispatched, dcb)
id
end

end
71 changes: 63 additions & 8 deletions src/platforms/apple/Impl.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module AppleImpl
# TODO

include("../common.jl")
include("../common_bind.jl")

include("./objc.jl")

Expand All @@ -17,7 +18,7 @@ function setup_platform()
# Register the yielder in the shared `NSApplication`.
app = get_shared_application()
@ccall class_replaceMethod(
a"NSApplication"cls::Ptr{Cvoid}, a"webviewsjlTick:"sel::Ptr{Cvoid},
a"NSApplication"cls::Ptr{Cvoid}, a"webviewsjlYielder:"sel::Ptr{Cvoid},
@cfunction(_event_loop_timeout, Cvoid, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}))::Ptr{Cvoid},
"v@:@"::Cstring
)::Ptr{Cvoid}
Expand All @@ -27,18 +28,19 @@ function setup_platform()
a"scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:"sel,
(TIMEOUT_INTERVAL / 1000)::Cdouble,
app,
a"webviewsjlTick:"sel,
a"webviewsjlYielder:"sel,
C_NULL,
true::Bool
)
prepare_timeout()
nothing
end

Base.@kwdef mutable struct Webview <: AbstractPlatformImpl
const parent_window::Ptr{Cvoid}
const debug::Bool
const callback_handler::CallbackHandler
const main_queue::ID = cglobal(:_dispatch_main_q)
const dispatched::Set{Base.RefValue{Tuple{Webview,Function}}} = Set()
window::ID = C_NULL
config::ID = C_NULL
manager::ID = C_NULL
Expand Down Expand Up @@ -77,7 +79,7 @@ end

API.window_handle(w::Webview) = w.window
# TODO: support multiple windows.
API.terminate(w::Webview) =
API.terminate(::Webview) =
let app = get_shared_application()
# Stop the main event loop instead of terminating the process.
@msg_send Cvoid app a"stop:"sel C_NULL
Expand All @@ -89,14 +91,20 @@ API.run(::Webview) =
let app = get_shared_application()
@msg_send Cvoid app a"run"sel
end

function _dispatch(ptr::Ptr{Cvoid})
call_dispatch(ptr)
clear_dispatch(ptr)
nothing
end

function API.dispatch(f::Function, w::Webview)
ref = Ref{Tuple{Webview,Function}}((w, f))
push!(w.dispatched, ref)
ptr = pointer_from_objref(ref)
cf = @cfunction(_dispatch, Cvoid, (Ptr{Cvoid},))
ptr = setup_dispatch(f, w.callback_handler)
@ccall dispatch_async_f(
w.main_queue::ID,
ptr::Ptr{Cvoid},
@cfunction((arg) -> (_dispatch(arg); nothing), Cvoid, (Ptr{Cvoid},))::Ptr{Cvoid}
cf::Ptr{Cvoid}
)::Cvoid
end

Expand Down Expand Up @@ -163,4 +171,51 @@ function API.eval!(w::Webview, js::AbstractString)
w
end

function _timeout(_1, _2, timer::ID)
valid = @msg_send Bool timer a"isValid"sel
valid || return
user_info = @msg_send ID timer a"userInfo"sel
fp = @msg_send Ptr{Cvoid} user_info a"pointerValue"sel
call_dispatch(fp)
interval = @msg_send Cdouble timer a"timeInterval"sel
interval > 0 || return
_clear_timeout(fp)
nothing
end

function prepare_timeout()
@ccall class_replaceMethod(
a"NSApplication"cls::Ptr{Cvoid}, a"webviewsjlTimeout:"sel::Ptr{Cvoid},
@cfunction(_timeout, Cvoid, (Ptr{Cvoid}, Ptr{Cvoid}, Ptr{Cvoid}))::Ptr{Cvoid},
"v@:@"::Cstring
)::Ptr{Cvoid}
nothing
end

function API.set_timeout(f::Function, w::Webview, interval::Real; repeat=false)
fp = setup_dispatch(f, w.callback_handler)
user_info = @msg_send ID a"NSValue"cls a"valueWithPointer:"sel fp
app = get_shared_application()
timer_id = @msg_send(
ID,
a"NSTimer"cls,
a"scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:"sel,
interval::Cdouble,
app,
a"webviewsjlTimeout:"sel,
user_info,
repeat::Bool
)
set_dispatch_id(fp, UInt64(timer_id))
fp
end

function _clear_timeout(timer_id::Ptr{Cvoid})
timer = clear_dispatch(timer_id)
isnothing(timer) && return
@msg_send Cvoid Ptr{Cvoid}(timer) a"invalidate"sel
nothing
end
API.clear_timeout(::Webview, timer_id::Ptr{Cvoid}) = _clear_timeout(timer_id)

end
Loading

2 comments on commit 54cdb9b

@sunoru
Copy link
Owner Author

@sunoru sunoru commented on 54cdb9b Dec 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/73637

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v1.0.0 -m "<description of version>" 54cdb9b605c4caaf6801ea70821aff8da890760d
git push origin v1.0.0

Please sign in to comment.