Closed grkek closed 3 years ago
Hi @grkek, thanks for using duktape.cr
!
Thanks for the example code, I haven't had a chance to run it yet, but plan to over the coming days.
At first glance, I wanted to mention a few things:
LibDUK.push_c_function
yourself, glad you got that figured out as it's rather low-level!LibDUK
methods are all just direct references to the native C implementation in Duktape. (link). So you're likely noticing behaviour in the Duktape library itself, not the Crystal bindings.nargs
parameter, Duktape will set the stack top (index 0) to be the first argument, followed by the next argument, etc.Thanks!
Hi @grkek, thanks for using
duktape.cr
!Thanks for the example code, I haven't had a chance to run it yet, but plan to over the coming days.
At first glance, I wanted to mention a few things:
* It looks like you're calling `LibDUK.push_c_function` yourself, glad you got that figured out as it's rather low-level! * The `LibDUK` methods are all just direct references to the native C implementation in Duktape. ([link](https://github.com/jessedoyle/duktape.cr/blob/master/src/lib_duktape.cr)). So you're likely noticing behaviour in the Duktape library itself, not the Crystal bindings. * Duktape does change the stack pointer in the scope of a C function call. There's documentation about it [here](https://duktape.org/api.html#duk_push_c_function). Basically when defining the `nargs` parameter, Duktape will set the stack top (index 0) to be the first argument, followed by the next argument, etc.
Thanks!
Hey, @jessedoyle the documentation is understandable, the problem is something else.
When I try to push a pointer to the stack it stays on the stack, but when I enter the push_c_function block, the stack just null's out while having the same context.
Hi @grkek, I tested your code snippet and noticed the issue.
I'm pretty sure Duktape heavily optimizes/manipulates the stack on a call into a native function. Your pointer reference may be getting cleaned up by the GC, but I think it's more likely an implementation detail in the engine itself.
Duktape uses the concept of "stash" objects (thread, heap, global stashes) to store native state. I was able to put together a proof of concept using stashes and boxing/unboxing to pass closure data to the native code:
require "duktape/runtime"
# our test closure proc
callable = ->(max : Int32) { puts "Max: #{max}, Random: #{rand(max)}" }
rt = Duktape::Runtime.new do |sbx|
# push proc pointer to the heap stash
sbx.push_heap_stash
sbx.push_pointer(Box.box(callable))
sbx.put_prop_string(-2, "callable")
sbx.push_global_proc("box") do |ptr|
env = Duktape::Sandbox.new(ptr)
# reconstitute the proc from heap stash pointer
env.push_heap_stash
env.get_prop_string(-1, "callable")
function = Box(Proc(Int32, Nil)).unbox(env.get_pointer(-1))
# call the proc
8.times { |i| function.call(i ** i) }
env.call_success
end
end
rt.call("box")
# Example Output:
#
# Max: 1, Random: 0
# Max: 1, Random: 0
# Max: 4, Random: 1
# Max: 27, Random: 19
# Max: 256, Random: 14
# Max: 3125, Random: 1960
# Max: 46656, Random: 7079
# Max: 823543, Random: 202953
This certainly falls under "advanced" functionality, but there's some documentation on stash objects here.
Please let me know if this pattern works for your use-case.
Thank you so much, @jessedoyle
@grkek - You're more than welcome!
One last thing to note is that you may need to use class variables to protect the reference from Crystal's GC. This is mentioned briefly in the docs here.
@grkek - You're more than welcome!
One last thing to note is that you may need to use class variables to protect the reference from Crystal's GC. This is mentioned briefly in the docs here.
A quick question, how does one retrieve the arguments passed to a function, for example I want a function to get 2 arguments and then retrieve them using the env.require_int 0
function.
How would one approach that?
@jessedoyle
Duktape will setup the stack top (index 0
) of an invoked function call to equal the first function argument value. Consecutive stack entries (indices 1+
) will be the values of the next function arguments in order.
When inside the scope of a native function call you can use the get_xxx
API methods to retrieve values from the stack (possibly returning nil
in Crystal), or the require_xxx
API methods to ensure the value is present on the stack and matches the expected type.
Here's the native function above modified to accept 2 arguments: iterations
and offset
:
require "duktape/runtime"
callable = ->(iteration : Int32, offset : Int32) do
content = {
iteration: iteration,
offset: offset,
random_call: "rand(#{iteration + offset})",
random_value: rand(iteration + offset)
}.to_json
puts content
end
rt = Duktape::Runtime.new do |sbx|
sbx.push_heap_stash
sbx.push_pointer(Box.box(callable))
sbx.put_prop_string(-2, "callable")
# call from JS like: box(4, 4)
# first argument is the number of iterations
# second argument is an integer offset
sbx.push_global_proc("box", 2) do |ptr|
env = Duktape::Sandbox.new(ptr)
iterations = env.require_int(0) # get argument 1
offset = env.require_int(1) # get argument 2
env.push_heap_stash
env.get_prop_string(-1, "callable")
function = Box(Proc(Int32, Int32, Nil)).unbox(env.get_pointer(-1))
iterations.times { |i| function.call(i, offset) }
env.call_success
end
end
rt.eval("box(4, 4);") # success
rt.eval("box();") # failure - Duktape::TypeError "type at 0 is not number"
When I try to box the value and it pass it in by pointers the stack gets reset after the push, is this a supposed behavior?