Fun bugs

Fun bugs

What is this

This is a list of fun bugs I’ve encontered while programming.
Note that these aren’t open problems, but fixed bugs.

C# delegate interop

// fragment of relevant code
public static class PulseNETInterop
{
    [DllImport(LibraryName)]
    public static extern IntPtr pulse_dotnet_create_stream(IntPtr ctx, IntPtr sink, [MarshalAs(UnmanagedType.LPStr)] string name, uint sample_rate, uint num_channels, uint target_buffer_size, IntPtr stream_write_cb, IntPtr userdata);

    public static IntPtr GetFunctionPointer<TDelegate>(TDelegate del) where TDelegate : Delegate
    {
        return Marshal.GetFunctionPointerForDelegate(del);
    }
}

// user code
public class SomeClass
{
    void SomeMethod()
    {
        var writeCb = PulseNETInterop.GetFunctionPointer(StreamWriteRequested);
        // After this call, writeCb will be called from native code
        _stream = PulseNETInterop.pulse_dotnet_create_stream(ctx, sinkString, "my_sink", SAMPLE_RATE, CHANNEL_COUNT, BUFFER_BYTES, writeCb, IntPtr.Zero);
    }
}

The application will just exit seemingly at random times.
There is so little code that I don’t think there is need for more hints. What’s wrong?

Reveal answer

Answer

var writeCb = PulseNETInterop.GetFunctionPointer(StreamWriteRequested);

This line implicitly creates a new delegate instance. This feature is called delegate inference I think.
Notice that this delegate is converted into a native pointer, but no reference to the delegate is kept on managed side.
If there is a lot of allocation/deallocation happening in your application, there is some probablility that the delegate gets garbage collected.
The native code will call into garbage collected delegate. Have fun debugging native code that calls into corrupted managed code!

C madness

Suppose you have C code that’s compiled into a shared library.

void stream_write_cb(pa_stream *p, size_t nbytes, void *userdata) {
    struct pulse_dotnet_stream *pdn_stream = (struct pulse_dotnet_stream*)userdata; 
    void *mem = malloc(nbytes);
    if (mem != NULL)
    {
        pdn_stream->write_cb(pdn_stream, mem, nbytes, pdn_stream->write_userdata);
        pa_stream_write(pdn_stream->stream, mem, nbytes, free, 0, PA_SEEK_RELATIVE);
    }
}

EXPORT struct pulse_dotnet_stream* pulse_dotnet_create_stream(
    struct pulse_dotnet_ctx *ctx, 
    const char *sink, 
    const char* name, 
    uint32_t sample_rate, 
    uint32_t num_channels, 
    uint32_t target_buffer_size, 
    stream_write_cb_t write_cb, 
    void *userdata)
{
    pa_sample_spec ss;
    ss.format = PA_SAMPLE_S16LE;
    ss.rate = sample_rate;
    ss.channels = num_channels;

    struct pulse_dotnet_stream *pdn_stream = NULL;
    for (int i = 0; i < MAX_STREAMS; i++)
    {
        if (ctx->streams[i].active == 0)
        {
            pdn_stream = ctx->streams + i;
            break;
        }
    }
    // max stream count reached
    if (pdn_stream == NULL)
    {
        return NULL;
    }

    pa_buffer_attr buffer_attr;
    buffer_attr.fragsize = (uint32_t)-1;
    buffer_attr.maxlength = (uint32_t)-1;
    buffer_attr.minreq = (uint32_t)-1;
    buffer_attr.tlength = target_buffer_size;
    buffer_attr.prebuf = target_buffer_size;

    pa_threaded_mainloop_lock(ctx->mainloop);
    pa_stream *stream = pa_stream_new(ctx->context, name, &ss, NULL);

    pdn_stream->stream = stream;
    pdn_stream->write_cb = write_cb;
    pdn_stream->write_userdata = userdata;
    pdn_stream->active = 1;

    pa_stream_set_write_callback(stream, stream_write_cb, pdn_stream);
    if (pa_stream_connect_playback(stream, sink, &buffer_attr, 0, NULL, NULL) != 0)
    {
        fprintf(stderr, "Failed to connect stream playback\n");
    }
    pa_threaded_mainloop_unlock(ctx->mainloop);
    return pdn_stream;
}

Suppose you have an user application using the shared library


void stream_write_cb(struct pulse_dotnet_stream *stream, uint8_t *buf, uint32_t nbytes, void *userdata)
{
    printf("write cb 1 %d\n", nbytes);
    int16_t *buf_16 = (int16_t*)buf;
    for (int i = 0; i < nbytes/2; i++)
    {
        buf_16[i] = (int16_t)(sinf(1.0f * 3.1415f * 1000.0f * phase) * 5000);
        phase += 1.0f / 48000.0f;
    }
}

int main(void)
{
    // ...
    struct pulse_dotnet_stream* stream = pulse_dotnet_create_stream(
        ctx, NULL, "TestAppSource", 48000, 2, 24000, stream_write_cb, NULL);
    // ...
}

Why is the printed argument in the stream_write_cb callback complete gibberish?

Reveal answer

Answer

void stream_write_cb(struct pulse_dotnet_stream *stream, uint8_t *buf, uint32_t nbytes, void *userdata)
void stream_write_cb(pa_stream *p, size_t nbytes, void *userdata)

Notice that these 2 have the same name?
Also they are missing static keyword.
Yup, they might be exported with default settings.
The call in the shared library gets linked to the callback in the user application, and the stream_write_cb in the shared library won’t ever be called!