New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature Request] - C extension for Ruby #28

Closed
xavriley opened this Issue Mar 18, 2016 · 6 comments

Comments

Projects
None yet
2 participants
@xavriley

xavriley commented Mar 18, 2016

Hi,

I realise this is a dumb thing to put in a Github issue but I couldn't see a way of contacting you specifically about this project, so I apologise in advance...

I'm working on a Ruby/music project called Sonic Pi which is pre-installed on the Raspberry Pi and is widely used in schools to teach music and coding to kids. Because we support the earlier B+ RPi (800MHz chip) we have to be sensitive about performance and one of the main bottlenecks for us is encoding/decoding OSC messages in Ruby to send from a Ruby server, both to/from SuperCollider and to/from the Qt GUI.

We use a lot of OSC messages and whilst we've optimised and benchmarked the pure Ruby version as much as possible (https://github.com/samaaron/sonic-pi/blob/master/app/server/sonicpi/lib/sonicpi/osc/oscencode.rb) it's still a bottleneck on the older RPis.

I had thought about trying to write an OSC library in mRuby and then use that via FFI which led me to your repos (mruby-osc). Given your experience with OSC and Ruby I thought it was worth a shot to see whether you thought that having a Ruby wrapper for this library (rtosc) was

a) possible?
b) desirable?

Even if you don't think it's a good idea I'd be interested to find out why and so on. In the past I've struggled to see how I could handle varargs from Ruby -> C and I was wondering if this library might have a solution for that.

Thanks for your time and your code.

@fundamental

This comment has been minimized.

Owner

fundamental commented Mar 18, 2016

On 03-18, Xavier Riley wrote:

I realise this is a dumb thing to put in a Github issue but I couldn't
see a way of contacting you specifically about this project, so I
apologise in advance...
This is a pretty typical use of the github issue tracker from what I've
seen, so don't worry about it.

I'm working on a Ruby/music project called Sonic Pi
It looks like a neat project and I've seen it pop up in discussions
quite a few times.

we support the earlier B+ RPi (800MHz chip) we have to be sensitive about performance
Yeah, that chip can be pretty slow.
I used either a model A or a ordinary B for a CI server for a while
before I had travis setup and they take their time for anything that's
compute bound.

and one of the main bottlenecks for us is encoding/decoding OSC
messages in Ruby to send from a Ruby server, both to/from
SuperCollider and to/from the Qt GUI.

We use a lot of OSC messages and whilst we've optimised and
benchmarked the pure Ruby version as much as possible
(https://github.com/samaaron/sonic-pi/blob/master/app/server/sonicpi/lib/sonicpi/osc/oscencode.rb)
it's still a bottleneck on the older RPis.
Interesting.
My own work with ZynAddSubFX tends to produce a large number of messages
(several thousand with casual use mere moments after starting up the
UI).
Serialization was never the bottleneck for me though, it was always
message dispatch (which will happen when you have a few million
possible callback paths).
I think I remember performance being pretty reasonable on the RPi board
that I have.

I had thought about trying to write an OSC library in mRuby and then
use that via FFI which led me to your repos (mruby-osc).
I'm not entirely clear as to why you'd be interested in mruby given that
sonic-pi seems to be based around having a MRI based installation.
I'm looking into mruby for easier binary distribution, but if you're
interested in using a C library within sonic-pi, then you'd want to
create a CRuby extension.
Building a CRuby extension shouldn't be all that difficult given the
amount of documentation out there, though you do already seem to have
libffi as part of the distribution, so that might not even be
necessary.

I've got some scraps sitting around for mruby-osc, but I got stuck when
trying to figure out how to handle instance specific memory buffers
through the mruby API (this would not be an issue in CRuby).

Given your
experience with OSC and Ruby I thought it was worth a shot to see
whether you thought that having a Ruby wrapper for this library
(rtosc) was

a) possible?
100%
b) desirable?
If you're limited by osc serialization and rtosc provides better
performance, then it sounds like it's desirable to your project.
Rtosc is built to be easily embeddable into other projects, so hopefully
it's relatively easy to pick out what chunks of code you need.

Even if you don't think it's a good idea I'd be interested to find out
why and so on. In the past I've struggled to see how I could handle
varargs from Ruby -> C and I was wondering if this library might have
a solution for that.

Assuming you want to use rtosc for just message serialization and
deserialization (forgetting bundles for the moment), then all the API
you need is:

typedef struct {
    int32_t len;
    uint8_t *data;
} rtosc_blob_t;

typedef union {
    int32_t       i;   //i,c,r
    char          T;   //I,T,F,N
    float         f;   //f
    double        d;   //d
    int64_t       h;   //h
    uint64_t      t;   //t
    uint8_t       m[4];//m
    const char   *s;   //s,S
    rtosc_blob_t  b;   //b
} rtosc_arg_t;

size_t rtosc_amessage(char        *buffer,
                      size_t       len,
                      const char  *address,
                      const char  *arguments,
                      const rtosc_arg_t *args);

typedef struct {
    const char    *type_pos;
    const uint8_t *value_pos;
} rtosc_arg_itr_t;

typedef struct {
    char type;
    rtosc_arg_t val;
} rtosc_arg_val_t;

rtosc_arg_itr_t rtosc_itr_begin(const char *msg);
rtosc_arg_val_t rtosc_itr_next(rtosc_arg_itr_t *itr);
int rtosc_itr_end(rtosc_arg_itr_t itr);

There's no var-args there (though the unions might be fun if you try
to use the ffi approach).
You can likely make a minimal C api which has methods with the basic
signatures of

  • RString *serialize(RString *path, RArray *args)
  • RArray *deserialize(RString *msg)
@xavriley

This comment has been minimized.

xavriley commented Mar 23, 2016

I just wanted to say - thanks so much for this detailed response. Up until now I had been thinking FFI was the only game in town for this kind of thing as I'd never written any C before, but your response inspired me to have a go at writing a C extension.

After some fiddling around with the Ruby C API for strings, I've made a start on a gem here https://github.com/xavriley/fast_osc It takes a Ruby string, runs it through rtosc and returns a Ruby array so the plumbing is there at least. It doesn't actually work properly yet (one of the arguments is missing in my test case) but that's beside the point at this stage! I'm hoping to flesh it out over time to make it more useful. Obviously it's very early days but I thought it better to follow up. Thanks again for your time and help with this.

@fundamental

This comment has been minimized.

Owner

fundamental commented Mar 24, 2016

just as a heads up it looks like you have a typo in some of your test data

/ab\x00ss\x00\x00foo\x00bar\x00

or (spaces and removed escapes for readability only)

/ab0 ss00 foo0 bar0

should actually be

/ab0 ,ss0 foo0 bar0

the comma is somewhat redundant, but the OSC spec requires it, so rtosc may parse arguments a little strangely if it is omitted.

@xavriley

This comment has been minimized.

xavriley commented Apr 30, 2016

Just wanted to say thanks for the heads up - I was silly to type those test messages from memory in retrospect.

With that error corrected it now returns the right number of args as expected. Given that this is a scaffold I can build on I'll keep chipping away at it. Thanks again for the help.

@xavriley

This comment has been minimized.

xavriley commented May 3, 2016

Another update here. I did some more hacking on this over the weekend and I now have basic functionality in place.

developer@e7c383641aab:~$ sudo gem install fast_osc
Fetching: fast_osc-0.0.4.gem (100%)
Building native extensions.  This could take a while...
Successfully installed fast_osc-0.0.4
1 gem installed
developer@e7c383641aab:~$ irb -r fast_osc
irb(main):001:0> FastOsc.serialize("/foo", ["beans", 1, 0.2])
=> "/foo\x00\x00\x00\x00,sif\x00\x00\x00\x00beans\x00\x00\x00\x00\x00\x00\x01>L\xCC\xCD"

The converse deserialize method works as expected. The source is over here: https://github.com/xavriley/fast_osc/ The benchmarks are suitably fast to live up to the name :)

I went down the route of vendoring rtosc.c and rtosc.h into my project. I've included the rtosc licence in the root of the project here https://github.com/xavriley/fast_osc/blob/master/LICENSE.txt but I just wanted to check - are you were happy with the licensing, vendoring and attribution in general?

One small point - I couldn't see a way to extract the address/path from a message - does that method exist in the rtosc.c file?

@fundamental

This comment has been minimized.

Owner

fundamental commented May 3, 2016

I'm glad to see that you're getting the performance boost that you were looking for.

Per the licensing that seems to be perfectly fine. Including the rtosc.{c,h} files into another project similar to how you have done so is how the library is designed to be used.

Extracting the path of an OSC message is pretty easy, so I didn't add any code for that task specifically.
The code to do so would effectively be

const char *rtosc_path(const char *msg)
{
     return msg;
}

Or in other words the path is the first null terminated string that makes up the message.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment