@cfunction help

Hello,

I have a tricky question. Let me know if there’s anything I can do to help explain etc.

I’m trying to make a fork of PortAudio that uses callback functions. You can see my progress here:

When I run the following code in test/runtests_local.jl

using PortAudio

PortAudioStream(input(), output()) do input_buffer, output_buffer, framecount, time_info, callback_flags, userdata
    return paComplete
end

I get a segfault:

fish: “/home/brandon/julia-1.6.0/bin/j…” terminated by signal SIGSEGV (Address boundary error)

I think the problem is most likely confusion on my part about how @cfunction works; this is the first time I’ve used it.

My code to wrap a Julia function as a PortAudio call-back function is below. I do a bit of conversion to allow users to access data rather than the underlying pointers.

function wrap_callback(callback, ::Type{UserData}, ::Type{Sample}) where {UserData, Sample}
    @cfunction(
        function (
            input_buffer_pointer, 
            output_buffer_pointer, 
            framecount,
            time_info_pointer,
            status_flags,
            user_data_pointer
        )
            PaStreamCallbackResult(callback(
                unsafe_wrap(Array, input_buffer_pointer, 2),
                unsafe_wrap(Array, output_buffer_pointer, 2),
                framecount,
                unsafe_pointer_to_objref(time_info_pointer),
                StreamCallbackFlags(status_flags),
                if user_data_pointer === C_NULL
                    nothing
                else
                    unsafe_pointer_to_objref(user_data_pointer)
                end
            ))
        end,
        PaStreamCallbackResult, # returns
        (
            Ptr{Sample}, # input buffer pointer
            Ptr{Sample}, # output buffer pointer
            Culong, # framecount
            Ptr{PaStreamCallbackTimeInfo}, # time info pointer
            PaStreamCallbackFlags, # status flags
            Ptr{UserData}, # userdata pointer
        )
    )
end

The PortAudio docs on writing a callback function are here:

http://portaudio.com/docs/v19-doxydocs/writing_a_callback.html

They state:

Your callback function must return an int and accept the exact parameters specified in this typedef:

typedef int PaStreamCallback( const void *input,
                                      void *output,
                                      unsigned long frameCount,
                                      const PaStreamCallbackTimeInfo* timeInfo,
                                      PaStreamCallbackFlags statusFlags,
                                      void *userData ) ;

Since it accepts a void *userData argument, you should use this to pass through the Julia function object, as explained here.

1 Like

@stevengj nice, that’s so clever! I tried updating the code but am still getting the same error.

My updated code looks like:

function run_true_callback(
    input_buffer_pointer, 
    output_buffer_pointer, 
    framecount,
    time_info_pointer,
    status_flags,
    user_data_pointer
)
    PaStreamCallbackResult((unsafe_pointer_to_objref(user_data_pointer)::Function)(
        unsafe_wrap(Array, input_buffer_pointer, 2),
        unsafe_wrap(Array, output_buffer_pointer, 2),
        framecount,
        unsafe_pointer_to_objref(time_info_pointer),
        StreamCallbackFlags(status_flags),
    ))
end

function make_dummy_callback(::Type{Sample}) where {Sample}
    @cfunction(
        run_true_callback,
        PaStreamCallbackResult, # returns
        (
            Ptr{Sample}, # input buffer pointer
            Ptr{Sample}, # output buffer pointer
            Culong, # framecount
            Ptr{PaStreamCallbackTimeInfo}, # time info pointer
            PaStreamCallbackFlags, # status flags
            Ptr{Nothing}
        )
    )
end

stream_pointer = Pa_OpenStream(
        input_parameters,
        output_parameters,
        the_sample_rate,
        frames_per_buffer,
        flag,
        make_dummy_callback(Sample),
        Ref(callback),
    )

And now I run

using PortAudio

PortAudioStream(input(), output()) do input_buffer, output_buffer, framecount, time_info, callback_flags
    return paComplete
end

After further investigation, it seems to be that doing almost anything inside the callback function results in a segfault.

function run_true_callback(
    input_buffer_pointer, 
    output_buffer_pointer, 
    framecount,
    time_info_pointer,
    status_flags,
    pass_through_pointer
)
    println("hi")
    return PaStreamCallbackResult(paComplete)
end
# “/home/brandon/julia-1.6.0/bin/j…” terminated by signal SIGSEGV (Address boundary error)
function run_true_callback(
    input_buffer_pointer, 
    output_buffer_pointer, 
    framecount,
    time_info_pointer,
    status_flags,
    pass_through_pointer
)
    @spawn println("hi")
    return PaStreamCallbackResult(paComplete)
end
# “/home/brandon/julia-1.6.0/bin/j…” terminated by signal SIGSEGV (Address boundary error)

So…, at a loss

Does it segfault if you use global cfunctions instead of creating them via the make_dummy_callback?

Currently, there is something wrong with closure-cfunctions.

This issue might be related: Segfault in ccall · Issue #40164 · JuliaLang/julia · GitHub

Something like this?

const CALLBACK_CFUNCTION = 
    @cfunction(
        run_true_callback,
        PaStreamCallbackResult, # returns
        (
            Ptr{Float32}, # input buffer pointer
            Ptr{Float32}, # output buffer pointer
            Culong, # framecount
            Ptr{PaStreamCallbackTimeInfo}, # time info pointer
            PaStreamCallbackFlags, # status flags
            Ptr{Nothing}, # pass-through
        )
    )

Yup, still get a segfault

Worth nothing that this simply returning a value does not throw a segfault, and runs as expected. Haven’t found anything else that works though.

function run_true_callback(
    input_buffer_pointer, 
    output_buffer_pointer, 
    framecount,
    time_info_pointer,
    status_flags,
    pass_through_pointer
)
    return PaStreamCallbackResult(paComplete)
end

I’ve discovered that repeatedly running and crashing gives a variety of error messages:

signal (11): Segmentation fault
in expression starting at REPL[1]:1
Assertion 'pthread_mutex_lock(&m->mutex) == 0' failed at ../src/pulsecore/mutex-posix.c:90, function pa_mutex_lock(). Aborting.

fatal: error thrown and no exception handler available.
DivideError()

signal (6): Aborted
in expression starting at REPL[1]:1

malloc(): smallbin double linked list corrupted

Just curious, but why are these Ptr{Float32} and not Ptr{Cvoid}? From my understanding, you’d want Cvoid here and convert whatever you get internally with one of the unsafe_ functions.

Those errors suggest some buffer overflow to me, which (I think) would be in line with what I said above.

Are you sure 2 for dims is correct here? This will lead to a 1-D Vector with 2 elements, not an array with 2 dimensions.

Hmm, well about the pointer type I’m honestly not sure. Could you explain a bit more when to use one and when to use the other? In any case, I tried changing to Cvoid and it didn’t seem to help.

I had figured out I was wrong about dims (dims is a confusing argument name), currently I’ve got:

unsafe_wrap(Array{Float32}, input_buffer_pointer, (2, framecount))

The two rows are for the left and right speaker I think. I’ll eventually have to figure out a way to pass the number of channels for other setups.

PortAudio seems to expect a *void, so from my understanding you’d have to use Ptr{Cvoid} and you’d want to convert that to Ptr{Float32} before passing it to unsafe_wrap(Array, ptr, ...).

I’m also not sure if you can do any of that at all:

/* This routine will be called by the PortAudio engine when audio is needed.
 * It may called at interrupt level on some machines so don't do anything
 * that could mess up the system like calling malloc() or free().
 */ 

So you’re not allowed to allocate at all.

Ok…, so, the kind of thing I might want to do is fill the output buffer with a generated sin wave. Is there a way to do that without allocating?

Also, at least as I’m reading the docs, being too greedy in the callback function shouldn’t cause crashes, just slowdowns:

For most modern systems, you won’t be able to cause crashes by making disallowed calls in the callback, but if you want your code to produce glitch-free audio, you will have to make sure you avoid function calls that may take an unbounded amount of time to execute

Well, I’m not sure if its software limitations or bugs, but I’m pretty sure I won’t be able to make any progress here…

That should be possible, having a loop calling sin again and again. That shouldn’t allocate.

I’m running out of ideas as well - maybe it’s because your enum isn’t using Cint as its basetype? what happens if you just return Cint(0) instead?

what’s paComplete? could you provide a MWE? I can give it a look if I could get some time.

Maybe it’s because your enum isn’t using Cint as its basetype?

At least in my current version, I call PaStreamCallbackResult to convert it back to a Cint.

That should be possible, having a loop calling sin again and again. That shouldn’t allocate.

Right, but how can I modify the data in the buffer without making an array?

could you provide a MWE? I can give it a look if I could get some time.

@Gnimuc that would be great! You can go to my fork for PortAudio (GitHub - bramtayl/PortAudio.jl: PortAudio wrapper for the Julia programming language, compatible with the JuliaAudio family of packages), check out the “callback” branch (I’ve given up on the master branch), and then include test/runtest_local.jl. It’s not exactly minimal, but you can experiment ways to make it more minimal (doing almost anything in the dummy callback function except returning causes a segfault on my end).

1 Like