Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 

Extensible Streams - Part 1

C++'s stream implementation may have missed the target in many ways, that doesn't mean extensible stream APIs in general are a bad idea. The problem we're trying to solve is providing an interface where one end doesn't need to know what's on the other end of a stream. C's standard library leaves a lot to wish for; there are extensions for custom FILE *-streams, but with spotty support.

Example:

struct hc_memory_stream s;
hc_memory_stream_init(&s);
hc_defer(hc_stream_deinit(&s.stream));
hc_printf(&s.stream, "%s%d", "foo", 42);
assert(strcmp("foo42", hc_memory_stream_string(&s)) == 0);

We'll start with defining the interface for a stream.

struct hc_stream {  
  size_t (*read)(struct hc_stream *, uint8_t *, size_t);
  size_t (*write)(struct hc_stream *, const uint8_t *, size_t);
  void (*deinit)(struct hc_stream *);
};

deinit, read & write delegate to respective stored function pointer.

size_t hc_read(struct hc_stream *s, uint8_t *data, const size_t n) {
  assert(s->read);
  return s->read(s, data, n);
}

size_t hc_write(struct hc_stream *s,
	        const uint8_t *data,
    	        const size_t n) {
  assert(s->write);
  return s->write(s, data, n);
}

getc is trivial to implement using read.

char hc_getc(struct hc_stream *s) {
  char c = 0;
  return hc_read(s, (uint8_t *)&c, 1) ? c : 0;
}

Which in turn allows us to easily implement gets, using a vector as buffer.

char *hc_gets(struct hc_stream *s, struct hc_malloc *malloc) {
  struct hc_vector out;
  hc_vector_init(&out, malloc, 1);

  for (;;) {
    char c = hc_getc(s);

    if (c == EOF) {
      break;
    }

    *(char *)hc_vector_push(&out) = c;

    if (c == '\n') {
      break;
    }
  }

 
  *(char *)hc_vector_push(&out) = 0;
  return (char *)out.start;
}

putc and puts delegate to write.

size_t hc_putc(struct hc_stream *s, const char data) {
  const uint8_t d[2] = {data, 0};
  return hc_write(s, d, 1);
}

size_t hc_puts(struct hc_stream *s, const char *data) {
  return hc_write(s, (const uint8_t *)data, strlen(data));
}

vprintf uses a temporary buffer to format the message.

size_t hc_vprintf(struct hc_stream *s,
	          const char *spec,
	          va_list args) {
  char *data = hc_vsprintf(spec, args);
  hc_defer(free(data));
  return hc_write(s, data, strlen(data));
}

size_t hc_printf(struct hc_stream *s, const char *spec, ...) {
  va_list args;
  va_start(args, spec);
  hc_defer(va_end(args));
  return hc_vprintf(s, spec, args);
}
void hc_stream_deinit(struct hc_stream *s) {
  assert(s->deinit);
  s->deinit(s);
}

We begin with file streams.

struct hc_file_stream_opts {
  bool close_file;
};

struct hc_file_stream {
  struct hc_stream stream;
  FILE *file;
  struct hc_file_stream_opts opts;
};

#define hc_file_stream_init(s, f, ...)					
  _hc_file_stream_init(s, f, (struct hc_file_stream_opts){		
      .close_file = false,						
      ##__VA_ARGS__							
    })

struct hc_file_stream *_hc_file_stream_init(struct hc_file_stream *s,
			 		    FILE *file,
					    struct hc_file_stream_opts opts) {
  s->stream = (struct hc_stream){
    .deinit = file_deinit,
    .read   = file_read,
    .write  = file_write,
  };

  s->file = file;
  s->opts = opts;
  return s;
};

read()/write() delegate to stdio

size_t file_read(struct hc_stream *s, uint8_t *data, size_t n) {
  struct hc_file_stream *fs = hc_baseof(s, struct hc_file_stream, stream);
  return fread(data, n, 1, fs->file);
}

size_t file_write(struct hc_stream *s, const uint8_t *data, size_t n) {
  struct hc_file_stream *fs = hc_baseof(s, struct hc_file_stream, stream);
  return fwrite(data, n, 1, fs->file);
}

If close_file is true, the file is closed with the stream.

void file_deinit(struct hc_stream *s) {
  struct hc_file_stream *fs = hc_baseof(s, struct hc_file_stream, stream);

  if (fs->close_file) {
    assert(fs->file);
  
    if (fclose(fs->file) == EOF) {
      hc_throw(0, "Failed closing file");
    }

    fs->file = NULL;
  }
}

Next up is memory streams. We'll use a vector to manage the stream data and add a separate variable to track the current read position.

struct hc_memory_stream {
  struct hc_stream stream;
  struct hc_vector data;
  size_t rpos;
};

struct hc_memory_stream *hc_memory_stream_init(struct hc_memory_stream *s,
                                               struct hc_malloc *malloc) {
  s->stream = (struct hc_stream){
    .deinit = memory_deinit,
    .read   = memory_read,
    .write  = memory_write,
  };

  hc_vector_init(&s->data, malloc, 1);
  s->rpos = 0;
  return s;
}

read() clamps the number of read bytes to the length of data - rpos, uses memcpy() to copy data, and finally updates rpos.

size_t memory_read(struct hc_stream *s, uint8_t *data, size_t n) {
  struct hc_memory_stream *ms = hc_baseof(s, struct hc_memory_stream, stream);

  if (ms->rpos + n > ms->data.length) {
    n = ms->data.length - ms->rpos;
  }
  
  memcpy(data, ms->data.start + ms->rpos, n);
  ms->rpos += n;
  return n;
}

write() inserts a block of n bytes at the end of the vector and uses memcpy() to copy data.

size_t memory_write(struct hc_stream *s, const uint8_t *data, size_t n) {
  struct hc_memory_stream *ms = hc_baseof(s, struct hc_memory_stream, stream);
  uint8_t *const dst = hc_vector_insert(&ms->data, ms->data.length, n);
  memcpy(dst, data, n);
  return n;
}

The stream data is freed in deinit().

void memory_deinit(struct hc_stream *s) {
  struct hc_memory_stream *ms = hc_baseof(s, struct hc_memory_stream, stream);
  hc_vector_deinit(&ms->data);
}