digital-fabric / polyphony

Fine-grained concurrency for Ruby
https://www.rubydoc.info/gems/polyphony
MIT License
658 stars 17 forks source link

Add support for raw buffers in backend #78

Closed noteflakes closed 2 years ago

noteflakes commented 2 years ago

For some use cases it would be beneficial to be able to do IO with raw buffers (i.e. pointer + size). Use cases include:

The first concrete usage I have in mind is for use in a custom OpenSSL BIO where the read / write hooks have the following signature:

int BIO_s_custom_write_ex(BIO *b, const char *data, size_t dlen, size_t *written);
int BIO_s_custom_read_ex(BIO *b, char *data, size_t dlen, size_t *readbytes);

The buffer is basically a wrapper for (data, size), so:

(Code adapted from this example.)

int BIO_s_custom_write_ex(BIO *b, const char *data, int dlen, size_t *written) {
  // the bio_props struct includes a ref to the io
  struct bio_props_t *bio_props = (struct bio_props_t)BIO_get_data(b);
  int ret = -1;
  VALUE buf = Buffer_new(data, dlen);

  VALUE argv[] = [bio_props->io, buf];

  VALUE res = Polyphony_backend_write(2, bio_props->io, buf, Qnil); // we can pass Qnil as self
  *written = INT2NUM(res);

  RB_GC_GUARD(buf);
}
noteflakes commented 2 years ago

Actually, implementing a Buffer class means we need to instantiate the buffer, a whole lot of ceremony at the Ruby runtime level (including GC work) that we don't actually need. The only difference between doing this and using plain Ruby strings is that we just remove the need to copy the data to the string buffer, which in the global level is a negligible cost.

An idea: since we want to minimize the overhead of buffers in both CPU time and memory, would there be a way to refrain from creating a Buffer instance? The idea is to pass a pointer to the buffer struct as a ruby FIXNUM. It's a bit of a hack but it should work (we should make sure this covers 64-bit pointers):

// in the callsite
struct polyphony_buffer buffer = {data, len};
...
VALUE res = Polyphony_backend_write(2, bio_props->io, FIX2NUM((uint)&buffer), Qnil);

// in the called method
...
if (TYPE(buf) == T_FIXNUM) {
  struct polyphony_buffer *buffer = (struct polyphony_buffer *)NUM2FIX(buf);
  written = write(fd, buffer->ptr, buffer->len);
}
...
noteflakes commented 2 years ago

This should also be highly beneficial to the planned deflate/gzip methods (#77).

noteflakes commented 2 years ago

In the future we might want to add support for raw buffers in Backend#chain, Backend#splice_chunks, but we leave that for now as the core functionality is there.