Communication between Julia and Javascript
After creating a Window and loading HTML and JS, you may want to interact with julia code (e.g. by clicking a button in HTML, or displaying a plot from julia).
This section covers this two-way communication.
Julia to Javascript
The easiest way to communicate to javascript from julia is with the @js
and @js_
macros. These macros allow you to execute arbitrary javascript code in a given Window.
julia> @js win x = 5;
julia> @js win x
5
The @js_
macro executes its code asynchronously, but doesn't return its result:
julia> @time @js win begin # Blocks until finished; `i` is returned
for i in 0:1000000 end # waste time
i # return i
end
0.261568 seconds (64.78 k allocations: 3.331 MiB)
1000001
julia> @time @js_ win begin # Returns immediately, but `i` is not returned.
for i in 0:1000000 end # waste time
i # This is ignored
end
0.008359 seconds (3.84 k allocations: 227.879 KiB)
Page(1, WebSocket(server, CONNECTED), Dict{String,Any}("webio" => Blink.AtomShell.var"#22#23"{Blink.AtomShell.WebIOBlinkComm}(Blink.AtomShell.WebIOBlinkComm(Window(1, Electron(Process(`/home/travis/build/JuliaGizmos/Blink.jl/deps/atom/electron /home/travis/build/JuliaGizmos/Blink.jl/src/AtomShell/main.js port 7474`, ProcessRunning), Sockets.TCPSocket(RawFD(0x00000016) active, 0 bytes waiting), Dict{String,Any}("callback" => Blink.var"#1#2"())), Page(#= circular reference @-5 =#), Task (done) @0x00007f9e922e5600))),"callback" => Blink.var"#1#2"()), Distributed.Future(1, 1, 1, Some(true)))
If your javascript expression is complex, or you want to copy-paste existing javascript, it can be easier to represent it as a pure javascript string. For that, you can call the js
function with a JSString
:
julia> body!(win, """<div id="box" style="color:red;"></div>""", async=false);
julia> div_id = "box";
julia> js(win, Blink.JSString("""document.getElementById("$div_id").style.color"""))
"red"
Note that the code passed to these macros runs in its own scope, so any javascript variables you create with var
(or the @var
equivalent for @js
) will be inaccessible after returning:
julia> @js win (@var x_var = 5; x_var) # x_var is only accessible within this scope.
5
julia> @js win x_var
ERROR: Javascript error ReferenceError: x_var is not defined
Javascript to Julia
Communication from javascript to julia currently works via a message passing interface.
To invoke julia code from javascript, you specify a julia callback via handle
:
julia> handle(w, "press") do args
@show args
end
This callback can then be triggered from javscript via Blink.msg()
:
julia> @js w Blink.msg("press", "Hello from JS");
args = "Hello from JS"
Note that the javascript function Blink.msg
takes exactly 1 argument. To pass more or fewer arguments, you must pass your arguments as an array from the JavaScript side (and optionally destructure them into variables on the Julia side):
julia> handle(w, "event1") do args # This can accept an array of arguments
@show args
end
#3 (generic function with 1 method)
julia> handle(w, "event2") do (count, values, message) # This will use the first 3 values from the passed array
@show count, values, message
end
#5 (generic function with 1 method)
julia> @js w Blink.msg("event1", [1, ['a','b'], "Hi"]);
args = Any[1, Any["a", "b"], "Hi"]
julia> @js w Blink.msg("event2", [1, ['a','b'], "Hi"]);
(count, values, message) = (1, Any["a", "b"], "Hi")
Finally, here is an example that uses a button to call back to julia:
julia> handle(w, "press") do arg
println(arg)
end
#1 (generic function with 1 method)
julia> body!(w, """<button onclick='Blink.msg("press", "HELLO")'>go</button>""", async=false);
Now, clicking the button will print HELLO
to julia's STDOUT.
Back-and-forth
Note that you cannot make a synchronous call to javascript from within a julia callback, or you'll cause julia to hang:
BAD:
julia> @js w x = 5
julia> handle(w, "press") do args...
# Increment x and get its new value
x = @js w (x += 1; x) # ERROR: Cannot make synchronous calls within a callback.
println("New value: $x")
end
#9 (generic function with 1 method)
julia> @js w Blink.msg("press", [])
# JULIA HANGS UNTIL CTRL-C, WHICH KILLS YOUR BLINK WINDOW.
GOOD: Instead, if you need to access the value of x
, you should simply provide it when invoking the press
handler:
julia> @js w x = 5
5
julia> handle(w, "press") do args...
x = args[1]
# Increment x
@js_ w (x = $x + 1) # Note the _asynchronous_ call.
println("New value: $x")
end
#3 (generic function with 1 method)
julia> @js w Blink.msg("press", x)
New value: 5
julia> # JULIA HANGS UNTIL CTRL-C, WHICH KILLS YOUR BLINK WINDOW.
Tasks
The julia webserver is implemented via Julia Tasks. This means that julia code invoked from javascript will run sort of in parallel to your main julia code.
In particular:
- Tasks are coroutines, not threads, so they aren't truly running in parallel.
- Instead, execution can switch between your code and the coroutine's code whenever a piece of computation is interruptible.
So, if your Blink callback handler performs uninterruptible work, it will fully occupy your CPU, preventing any other computation from occuring, and can potentially hang your computation.
Examples:
BAD: If your callback runs a long loop, it won't be uninterruptible while it's running:
julia> handle(w, "press") do args...
println("Start")
while true end # infinite loop
println("End")
end
#40 (generic function with 1 method)
julia> body!(w, """<button onclick='Blink.msg("press", 1)'>go</button>""", async=false);
julia> # CLICK THE go BUTTON, AND YOUR PROCESS WILL FREEZE
Start
BAD: The same is true if your main julia computation is hogging the CPU, then your callback can't run:
julia> handle(w, "press") do args...
println("Start")
sleep(5) # This will happily yield to any other computation.
println("End")
end
#41 (generic function with 1 method)
julia> body!(w, """<button onclick='Blink.msg("press", 1)'>go</button>""", async=false);
julia> while true end # Infinite loop
# NOW, CLICK THE go BUTTON, AND NOTHING HAPPENS, SINCE THE CPU IS BEING HOGGED!
GOOD: So to allow for happy communication, all your computations should be interruptible, which you can achieve with calls such as yield
, or sleep
:
julia> handle(w, "press") do args...
println("Start")
sleep(5) # This will happily yield to any other computation.
println("End")
end
#39 (generic function with 1 method)
julia> body!(w, """<button onclick='Blink.msg("press", 1)'>go</button>""", async=false);
julia> while true # Still an infinite loop, but a _fair_ one.
yield() # This will yield to any other computation, allowing the callback to run.
end
# NOW, CLICKING THE go BUTTON WILL WORK CORRECTLY ✅
Start
End