diff --git a/buffer.c b/buffer.c index 4c5bc1bf..d8f79e46 100644 --- a/buffer.c +++ b/buffer.c @@ -150,7 +150,7 @@ static struct evbuffer_chain *evbuffer_expand_singlechain(struct evbuffer *buf, static int evbuffer_ptr_subtract(struct evbuffer *buf, struct evbuffer_ptr *pos, size_t howfar); static int evbuffer_file_segment_materialize(struct evbuffer_file_segment *seg); - +static inline void evbuffer_chain_incref(struct evbuffer_chain *chain); static struct evbuffer_chain * evbuffer_chain_new(size_t size) @@ -178,17 +178,29 @@ evbuffer_chain_new(size_t size) */ chain->buffer = EVBUFFER_CHAIN_EXTRA(u_char, chain); + chain->refcnt = 1; + return (chain); } static inline void evbuffer_chain_free(struct evbuffer_chain *chain) { + EVUTIL_ASSERT(chain->refcnt > 0); + if (--chain->refcnt > 0) { + // chain is still referenced by other chains + return; + } + if (CHAIN_PINNED(chain)) { + // will get freed once no longer dangling + chain->refcnt++; chain->flags |= EVBUFFER_DANGLING; return; } - + + // safe to release chain, it's either a referencing + // chain or all references to it have been freed if (chain->flags & EVBUFFER_REFERENCE) { struct evbuffer_chain_reference *info = EVBUFFER_CHAIN_EXTRA( @@ -212,6 +224,21 @@ evbuffer_chain_free(struct evbuffer_chain *chain) evbuffer_file_segment_free(info->segment); } } + if (chain->flags & EVBUFFER_MULTICAST) { + struct evbuffer_multicast_parent *info = + EVBUFFER_CHAIN_EXTRA( + struct evbuffer_multicast_parent, + chain); + // referencing chain is being freed, decrease + // refcounts of source chain and associated + // evbuffer (which get freed once both reach + // zero) + EVUTIL_ASSERT(info->source != NULL); + EVUTIL_ASSERT(info->parent != NULL); + EVBUFFER_LOCK(info->source); + evbuffer_chain_free(info->parent); + _evbuffer_decref_and_unlock(info->source); + } mm_free(chain); } @@ -316,6 +343,12 @@ _evbuffer_chain_unpin(struct evbuffer_chain *chain, unsigned flag) evbuffer_chain_free(chain); } +static inline void +evbuffer_chain_incref(struct evbuffer_chain *chain) +{ + ++chain->refcnt; +} + struct evbuffer * evbuffer_new(void) { @@ -853,6 +886,46 @@ APPEND_CHAIN(struct evbuffer *dst, struct evbuffer *src) dst->total_len += src->total_len; } +static inline void +APPEND_CHAIN_MULTICAST(struct evbuffer *dst, struct evbuffer *src) +{ + struct evbuffer_chain *tmp; + struct evbuffer_chain *chain = src->first; + struct evbuffer_multicast_parent *extra; + + ASSERT_EVBUFFER_LOCKED(dst); + ASSERT_EVBUFFER_LOCKED(src); + + for (; chain; chain = chain->next) { + if (!chain->off || chain->flags & EVBUFFER_DANGLING) { + // skip empty chains + continue; + } + + tmp = evbuffer_chain_new(sizeof(struct evbuffer_multicast_parent)); + if (!tmp) { + event_warn("%s: out of memory", __func__); + return; + } + extra = EVBUFFER_CHAIN_EXTRA(struct evbuffer_multicast_parent, tmp); + // reference evbuffer containing source chain so it + // doesn't get released while the chain is still + // being referenced to + _evbuffer_incref(src); + extra->source = src; + // reference source chain which now becomes immutable + evbuffer_chain_incref(chain); + extra->parent = chain; + chain->flags |= EVBUFFER_IMMUTABLE; + tmp->buffer_len = chain->buffer_len; + tmp->misalign = chain->misalign; + tmp->off = chain->off; + tmp->flags |= EVBUFFER_MULTICAST|EVBUFFER_IMMUTABLE; + tmp->buffer = chain->buffer; + evbuffer_chain_insert(dst, tmp); + } +} + static void PREPEND_CHAIN(struct evbuffer *dst, struct evbuffer *src) { @@ -917,6 +990,49 @@ done: return result; } +int +evbuffer_add_buffer_reference(struct evbuffer *outbuf, struct evbuffer *inbuf) +{ + size_t in_total_len, out_total_len; + struct evbuffer_chain *chain; + int result = 0; + + EVBUFFER_LOCK2(inbuf, outbuf); + in_total_len = inbuf->total_len; + out_total_len = outbuf->total_len; + chain = inbuf->first; + + if (in_total_len == 0) + goto done; + + if (outbuf->freeze_end || outbuf == inbuf) { + result = -1; + goto done; + } + + for (; chain; chain = chain->next) { + if ((chain->flags & (EVBUFFER_FILESEGMENT|EVBUFFER_SENDFILE|EVBUFFER_MULTICAST)) != 0) { + // chain type can not be referenced + result = -1; + goto done; + } + } + + if (out_total_len == 0) { + /* There might be an empty chain at the start of outbuf; free + * it. */ + evbuffer_free_all_chains(outbuf->first); + } + APPEND_CHAIN_MULTICAST(outbuf, inbuf); + + outbuf->n_add_for_cb += in_total_len; + evbuffer_invoke_callbacks(outbuf); + +done: + EVBUFFER_UNLOCK2(inbuf, outbuf); + return result; +} + int evbuffer_prepend_buffer(struct evbuffer *outbuf, struct evbuffer *inbuf) { diff --git a/evbuffer-internal.h b/evbuffer-internal.h index 6c3838e1..7bde8a16 100644 --- a/evbuffer-internal.h +++ b/evbuffer-internal.h @@ -185,6 +185,11 @@ struct evbuffer_chain { /** a chain that should be freed, but can't be freed until it is * un-pinned. */ #define EVBUFFER_DANGLING 0x0040 + /** a chain that is a referenced copy of another chain */ +#define EVBUFFER_MULTICAST 0x0080 + + /** number of references to this chain */ + int refcnt; /** Usually points to the read-write memory belonging to this * buffer allocated as part of the evbuffer_chain allocation. @@ -243,6 +248,15 @@ struct evbuffer_file_segment { ev_off_t length; }; +/** Information about the multicast parent of a chain. Lives at the + * end of an evbuffer_chain with the EVBUFFER_MULTICAST flag set. */ +struct evbuffer_multicast_parent { + /** source buffer the multicast parent belongs to */ + struct evbuffer *source; + /** multicast parent for this chain */ + struct evbuffer_chain *parent; +}; + #define EVBUFFER_CHAIN_SIZE sizeof(struct evbuffer_chain) /** Return a pointer to extra data allocated along with an evbuffer. */ #define EVBUFFER_CHAIN_EXTRA(t, c) (t *)((struct evbuffer_chain *)(c) + 1) diff --git a/include/event2/buffer.h b/include/event2/buffer.h index 0e31bb8a..f99e9a5b 100644 --- a/include/event2/buffer.h +++ b/include/event2/buffer.h @@ -437,6 +437,22 @@ char *evbuffer_readln(struct evbuffer *buffer, size_t *n_read_out, */ int evbuffer_add_buffer(struct evbuffer *outbuf, struct evbuffer *inbuf); +/** + Copy data from one evbuffer into another evbuffer. + + This is a non-destructive add. The data from one buffer is copied + into the other buffer. However, no unnecessary memory copies occur. + + Note that buffers already containing buffer references can't be added + to other buffers. + + @param outbuf the output buffer + @param inbuf the input buffer + @return 0 if successful, or -1 if an error occurred + */ +int evbuffer_add_buffer_reference(struct evbuffer *outbuf, + struct evbuffer *inbuf); + /** A cleanup function for a piece of memory added to an evbuffer by reference. diff --git a/test/regress_buffer.c b/test/regress_buffer.c index 21fdf060..660ffc77 100644 --- a/test/regress_buffer.c +++ b/test/regress_buffer.c @@ -1596,6 +1596,111 @@ end: evbuffer_free(buf2); } +static void +test_evbuffer_multicast(void *ptr) +{ + const char chunk1[] = "If you have found the answer to such a problem"; + const char chunk2[] = "you ought to write it up for publication"; + /* -- Knuth's "Notes on the Exercises" from TAOCP */ + char tmp[16]; + size_t len1 = strlen(chunk1), len2=strlen(chunk2); + + struct evbuffer *buf1 = NULL, *buf2 = NULL; + + buf1 = evbuffer_new(); + tt_assert(buf1); + + evbuffer_add(buf1, chunk1, len1); + evbuffer_add(buf1, ", ", 2); + evbuffer_add(buf1, chunk2, len2); + tt_int_op(evbuffer_get_length(buf1), ==, len1+len2+2); + + buf2 = evbuffer_new(); + tt_assert(buf2); + + tt_int_op(evbuffer_add_buffer_reference(buf2, buf1), ==, 0); + // nested references are not allowed + tt_int_op(evbuffer_add_buffer_reference(buf2, buf2), ==, -1); + tt_int_op(evbuffer_add_buffer_reference(buf1, buf2), ==, -1); + + // both buffers contain the same amount of data + tt_int_op(evbuffer_get_length(buf1), ==, evbuffer_get_length(buf1)); + + /* Make sure we can drain a little from the first buffer. */ + tt_int_op(evbuffer_remove(buf1, tmp, 6), ==, 6); + tt_int_op(memcmp(tmp, "If you", 6), ==, 0); + tt_int_op(evbuffer_remove(buf1, tmp, 5), ==, 5); + tt_int_op(memcmp(tmp, " have", 5), ==, 0); + + /* Make sure that prepending does not meddle with immutable data */ + tt_int_op(evbuffer_prepend(buf1, "I have ", 7), ==, 0); + tt_int_op(memcmp(chunk1, "If you", 6), ==, 0); + evbuffer_validate(buf1); + + /* Make sure we can drain a little from the second buffer. */ + tt_int_op(evbuffer_remove(buf2, tmp, 6), ==, 6); + tt_int_op(memcmp(tmp, "If you", 6), ==, 0); + tt_int_op(evbuffer_remove(buf2, tmp, 5), ==, 5); + tt_int_op(memcmp(tmp, " have", 5), ==, 0); + + /* Make sure that prepending does not meddle with immutable data */ + tt_int_op(evbuffer_prepend(buf2, "I have ", 7), ==, 0); + tt_int_op(memcmp(chunk1, "If you", 6), ==, 0); + evbuffer_validate(buf2); + + /* Make sure the data can be read from the second buffer when the first is freed */ + evbuffer_free(buf1); + buf1 = NULL; + + tt_int_op(evbuffer_remove(buf2, tmp, 6), ==, 6); + tt_int_op(memcmp(tmp, "I have", 6), ==, 0); + + tt_int_op(evbuffer_remove(buf2, tmp, 6), ==, 6); + tt_int_op(memcmp(tmp, " foun", 6), ==, 0); + +end: + if (buf1) + evbuffer_free(buf1); + if (buf2) + evbuffer_free(buf2); +} + +static void +test_evbuffer_multicast_drain(void *ptr) +{ + const char chunk1[] = "If you have found the answer to such a problem"; + const char chunk2[] = "you ought to write it up for publication"; + /* -- Knuth's "Notes on the Exercises" from TAOCP */ + size_t len1 = strlen(chunk1), len2=strlen(chunk2); + + struct evbuffer *buf1 = NULL, *buf2 = NULL; + + buf1 = evbuffer_new(); + tt_assert(buf1); + + evbuffer_add(buf1, chunk1, len1); + evbuffer_add(buf1, ", ", 2); + evbuffer_add(buf1, chunk2, len2); + tt_int_op(evbuffer_get_length(buf1), ==, len1+len2+2); + + buf2 = evbuffer_new(); + tt_assert(buf2); + + tt_int_op(evbuffer_add_buffer_reference(buf2, buf1), ==, 0); + tt_int_op(evbuffer_get_length(buf2), ==, len1+len2+2); + tt_int_op(evbuffer_drain(buf1, evbuffer_get_length(buf1)), ==, 0); + tt_int_op(evbuffer_get_length(buf2), ==, len1+len2+2); + tt_int_op(evbuffer_drain(buf2, evbuffer_get_length(buf2)), ==, 0); + evbuffer_validate(buf1); + evbuffer_validate(buf2); + +end: + if (buf1) + evbuffer_free(buf1); + if (buf2) + evbuffer_free(buf2); +} + /* Some cases that we didn't get in test_evbuffer() above, for more coverage. */ static void test_evbuffer_prepend(void *ptr) @@ -2012,6 +2117,8 @@ struct testcase_t evbuffer_testcases[] = { { "search", test_evbuffer_search, 0, NULL, NULL }, { "callbacks", test_evbuffer_callbacks, 0, NULL, NULL }, { "add_reference", test_evbuffer_add_reference, 0, NULL, NULL }, + { "multicast", test_evbuffer_multicast, 0, NULL, NULL }, + { "multicast_drain", test_evbuffer_multicast_drain, 0, NULL, NULL }, { "prepend", test_evbuffer_prepend, TT_FORK, NULL, NULL }, { "peek", test_evbuffer_peek, 0, NULL, NULL }, { "freeze_start", test_evbuffer_freeze, 0, &nil_setup, (void*)"start" },