Skip to content

Commit

Permalink
solves memory issue by disabling GC in Crystal callback, otherwise GC…
Browse files Browse the repository at this point in the history
… may initiate in a different thread (NodeJS process thread) and that will cause fatal error in GC
  • Loading branch information
Jens committed May 1, 2019
1 parent 5a8ed8a commit 310c349
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 41 deletions.
13 changes: 7 additions & 6 deletions js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@

const addon = require('../build/libjack2.node');


const snooze = ms => new Promise(resolve => setTimeout(resolve, ms));

var time = 0
async function process_callback(nframes, out1, out2) {
console.log("time" + time)
async function process_callback(time_c, nframes, out1, out2) {
console.log("time js: " + time-time_c)
f = 440.0;
sr = 44000;

for (var i=0; i<nframes; i++) {

val = (Math.sin( time/(sr/f) * (2*Math.PI) )+1)/2;
val = (Math.sin( (time%sr)/(sr/f) * (2*Math.PI) )+1)/2;
out1[i]=val;
time+=1
}
Expand All @@ -22,4 +22,5 @@ console.log(addon.setup(process_callback));

setInterval(function() {

}, 1000000);
}, 1000000);

29 changes: 14 additions & 15 deletions src/libjack2.cr
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ module Jack2
VERSION = "0.1.0"

class OutputFrame
getter :nframes, :out1, :out2
setter :nframes, :out1, :out2
getter :nframes, :out1, :out2, :time
setter :nframes, :out1, :out2, :time

def initialize(@nframes : Int32 = 0, @out1 : Float32* = Pointer(Float32).null, @out2 : Float32* = Pointer(Float32).null)
def initialize(@time : Int32 = 0, @nframes : Int32 = 0, @out1 : Float32* = Pointer(Float32).null, @out2 : Float32* = Pointer(Float32).null)
end
end

Expand All @@ -19,8 +19,6 @@ module Jack2
@ports = {} of String => Jack2_c::JackPort*
@@frame = OutputFrame.new

@time = 0

def initialize
status = Jack2_c::JackStatus::JackFailure
@client = Jack2_c.jack_client_open("testclient", Jack2_c::JackOptions::JackNullOption, pointerof(status)) ||
Expand All @@ -46,15 +44,6 @@ module Jack2
@ports[name] = port
end

def process(nframes, &block : (OutputFrame -> Void))
@@frame.nframes = nframes
@@frame.out1 = Jack2_c.jack_port_get_buffer(@ports["out1"], nframes).as(Float32*)
@@frame.out2 = Jack2_c.jack_port_get_buffer(@ports["out2"], nframes).as(Float32*)

block.call(@@frame)
return 0
end

def connect(p1, p2)
case Jack2_c.jack_connect(@client, p1, p2)
when 0
Expand All @@ -65,20 +54,30 @@ module Jack2

def register_process_callback(&block : (OutputFrame -> Void))
closure = ->(nframes : Jack2_c::JackNframesT, data : Void*) {
process(nframes, &block)
puts "time from crystal: #{@@frame.time}"
@@frame.nframes = nframes
@@frame.time = @@frame.time + nframes
@@frame.out1 = Jack2_c.jack_port_get_buffer(@ports["out1"], nframes).as(Float32*)
@@frame.out2 = Jack2_c.jack_port_get_buffer(@ports["out2"], nframes).as(Float32*)

block.call(@@frame)
}

# Make sure garbagem collector is not running
# on the boxed closure
@@closure_callback = Box.box(closure)

Jack2_c.jack_set_process_callback(@client, ->(nframes : Jack2_c::JackNframesT, data : Void*) {
# Make sure GC is not running within another thread
GC.disable
begin
callback = Box(typeof(closure)).unbox(data)
callback.call(nframes, data)
rescue e
puts "Exception within process callback: #{e}"
return 1
ensure
GC.enable
end
0
}, @@closure_callback)
Expand Down
43 changes: 23 additions & 20 deletions src/libnode.cr
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,17 @@ module Node
VERSION = "0.1.0"

@@module = Node_c::NapiModule.new
@@client = ::Jack2::Client.new
@@call_js_cb : Pointer(Void)?

def self.setup
module_register_callback = ->(env : Node_c::NapiEnv, exports : Node_c::NapiValue) {
# Avoid closure
register_fn("setup", ->(env : Node_c::NapiEnv, info : Node_c::NapiCallbackInfo) {
begin
client = ::Jack2::Client.new
client.register_port("out1", ::Jack2::Client::PORT_OUTPUT)
client.register_port("out2", ::Jack2::Client::PORT_OUTPUT)
puts client.ports
@@client.register_port("out1", ::Jack2::Client::PORT_OUTPUT)
@@client.register_port("out2", ::Jack2::Client::PORT_OUTPUT)
puts @@client.ports

# Get arguments
argc = 1_u64
Expand All @@ -110,45 +111,47 @@ module Node

# Create threadsafe function
call_js_cb = ->(env : Node_c::NapiEnv, cb : Node_c::NapiValue, ctx : Void*, frame : Void*) {
# This is called within a NodeJS UV Process context
begin
GC.disable
recv = uninitialized Node_c::NapiValue
frame = Box(::Jack2::OutputFrame).unbox(frame)

typed_out1 = napi_typedarray(frame.out1, frame.nframes)
typed_out2 = napi_typedarray(frame.out2, frame.nframes)

args = uninitialized Node_c::NapiValue[3]
args = uninitialized Node_c::NapiValue[4]
nframes = frame.nframes
args[0] = napi_int(nframes)
args[1] = typed_out1
args[2] = typed_out2
args[0] = napi_int(frame.time)
args[1] = napi_int(frame.nframes)
args[2] = napi_typedarray(frame.out1, frame.nframes)
args[3] = napi_typedarray(frame.out2, frame.nframes)

assert_napiok(Node_c.napi_get_undefined(env, pointerof(recv)))
assert_napiok(Node_c.napi_call_function(env, recv, cb, 3, args.to_unsafe, nil))
assert_napiok(Node_c.napi_call_function(env, recv, cb, 4, args.to_unsafe, nil))
return # Void
rescue e
puts e.to_s
ensure
GC.enable
end
}
assert(call_js_cb.closure?)
assert_napiok(Node_c.napi_create_threadsafe_function(env, callback_fn,
nil, napi_string("callback for jack2 process"), 20, 1, nil, nil, nil, call_js_cb, pointerof(threadsafe_function)))

# Callback definition (will call jack_process->jack_closure->closure below->call_js_cb->function)
client.register_process_callback do |frame|

@@client.register_process_callback do |frame|
# This is jack_c process thread (GC is disabled already)
assert_napiok(Node_c.napi_acquire_threadsafe_function(threadsafe_function))
assert_napiok(Node_c.napi_call_threadsafe_function(threadsafe_function, Box.box(frame),
Node_c::NapiThreadsafeFunctionCallMode::NapiTsfnBlocking))
assert_napiok(Node_c.napi_release_threadsafe_function(threadsafe_function,
Node_c::NapiThreadsafeFunctionReleaseMode::NapiTsfnRelease))
end
client.activate
client.connect("testclient:out1", "Live:in1")
client.connect("testclient:out2", "Live:in2")
puts client.ports

ports = client.ports
@@client.activate
@@client.connect("testclient:out1", "Live:in1")
@@client.connect("testclient:out2", "Live:in2")

return napi_array(ports)
return napi_array(@@client.ports)
rescue e
napi_throw("Exception from Crystal: #{e.to_s}")
end
Expand Down
4 changes: 4 additions & 0 deletions src/tools.cr
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ end

macro log_debug(str)
end

macro assert(eval)
raise "Assert failed: {{eval}}" if {{eval}}
end

0 comments on commit 310c349

Please sign in to comment.