Skip to content

pseusys/betterbuf

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

BetterBuf

Go Reference

Originally, a part of SeasideVPN project.

Ever heard of sync.Pool structure? It allows faster allocation and buffering of large memory chunks. But everything comes with a price: you should always return to the pool exactly the same pointer as was retrieved from there. It is still fine and convenient if the buffer retrieved from the pool can be processed within one function. But what if it's not the case?

This package resolves this issue by providing a special type: Buffer, that works similarly to a normal Go byte slice, but also:

  1. It always stores the pointer to the original slice, retrieved from Pool.
  2. It allows easy appending and prepending bytes.
  3. It never does reallocation.

That's it! Enjoy safe and fast memory allocation with Buffers and Pools!

Suggested use cases

Buffers and Pools work best in environments where heavy, complex and fast data processing is required. They allow avoiding time-inefficient operations (such as allocation and copying) as much as possible, still maintaining convenient API. However, it is important to always be sure the allocated Buffer size and capacity would be sufficient, as they can not be increased later. A few examples of such environments are:

  1. Network packet processing (well-defined maximum packet size, header prepending might be required).
  2. Cryptography (cipher overhead is limited in size).

BetterBuf API

Find below a short description of betterbuf possible usage cases. Please, see function and structure documentation for more detailed information.

Create a buffer

Create customized pool as a shared object:

// Message buffers retrieved from the pool will have:
// - Forward (right, appending) capacity of 128
// - Backward (left, prepending) capacity of 24
var MessagePool = betterbuf.CreateBufferPool(24, 128)

Get a message buffer from the pool:

// Message buffer is 10 bytes long
messageBuf := MessagePool.Get(10)

// Message buffer is 128 bytes long
fullBuf := MessagePool.GetFull()

Or create a buffer directly:

mySlice := make([]byte, 100)

// Buffer made from the given slice, no allocation happens
myBuffer := betterbuf.NewBufferFromSlice(mySlice)

// Buffer with given capacity and contents of the given slice, an allocation and a copy happen
bufferWithCapacity := betterbuf.NewBufferFromCapacityEnsured(mySlice, 10, 10)

// An empty buffer is allocated, with given size and capacity, allocation happens
clearBuffer := betterbuf.NewClearBuffer(100, 10, 10)

// Same as before, but allocated buffer is empty, allocation happens
emptyBuffer := betterbuf.NewEmptyBuffer(10, 10)

// A buffer is allocated and filled with random values, no additional capacity added, allocation and reading random bytes happen
randomBuffer, err := betterbuf.NewRandomBuffer(100)

Read buffer properties

Retrieve buffer information:

// Buffer available length is returned
length := myBuffer.Length()

// Buffer forward (right, appending) capacity is returned
forward := myBuffer.ForwardCap()

// Buffer backward (left, prepending) capacity is returned
backward := myBuffer.BackwardCap()

Slice compatibility

Current slice value of the buffer can be extracted with Slice method:

// Value of buffer will be retrieved, from start to end, underlying capacity won't be taken into account
mySlice := myBuffer.Slice()

Just like with slices, Buffers expose methods for getting and setting certain bytes:

// Retrieve byte 0 of the buffer
myByte := myBuffer.Get(0)

// Set byte 0 of the buffer to value 42
myBuffer.Set(0, 42)

Re-slice and re-buffer

Just like a sub-slice can be received from slice with slice[start:end], same functionality is available for Buffers. Visible starting point of the Buffer can be moved forward, ending point can be moved backward. No allocation or copying ever happens here:

// The new buffer will start from the 2nd byte of the original one, the 2 bytes will be added to it's forward capacity.
myForwardBuffer := myBuffer.RebufferStart(2)

// The new buffer will end at the 12th byte of the original one, the remaining bytes will be added to it's backward capacity. 
myBackwardBuffer := myBuffer.RebufferEnd(12)

// The two previous calls can be combined into one.
myShrunkBuffer := myBuffer.Rebuffer(2, 12)

NB! Similar method shortcuts returning slices instead of Buffers are available: ResliceStart, ResliceEnd and Reslice.

Prepending and appending slices and buffers

Similarly to append built-in function for slices, special methods are available for Buffers. However, instead of re-allocation, these methods raise errors in case buffer capacity is not enough. No allocation ever happens here, but every time a byte slice is copied:

// The other buffer will be appended to the current buffer, an error will be thrown in case of insufficient backward capacity.
appendedBuffer, err := myBuffer.AppendBuffer(otherBuffer)

// The other buffer will be prepended to the current buffer, an error will be thrown in case of insufficient forward capacity.
prependedBuffer, err := myBuffer.PrependBuffer(otherBuffer)

NB! Similar method shortcuts accepting slices instead of Buffers are available: AppendSlice and PrependSlice.

Expanding slices and buffers

Finally, just like re-slice and re-buffer decreases effectively available Buffer size, special methods are available for increasing it. No allocation or copying ever happens here:

// The new buffer will start 5 bytes before the original one, if backward capacity is not enough an error will be thrown.
myForwardBuffer, err := myBuffer.ExpandBefore(10)

// The new buffer will start 10 bytes after the original one, if forward capacity is not enough an error will be thrown.
myBackwardBuffer, err := myBuffer.ExpandAfter(10)

// The two previous calls can be combined into one.
myExpendedBuffer, err := myBuffer.Expand(5, 10)

Ensure buffer was not changed

Sometimes it's impossible to always use Buffers, and it's required to fall back to slice API. In these cases, slice reallocation might occur, which can break safe memory allocation model provided by Buffers. For instance, crypto/cipher AEAD.Seal method can reuse plaintext's storage for the encrypted output. Special methods should be used that case to verify that no reallocation happened - and return an error otherwise:

var plaintext, nonce Buffer
ciphertext := AEAD.Seal(plaintext.ResliceEnd(0), nonce.Slice(), plaintext.Slice(), nil)

// Check if no reallocation was made - or at least it didn't expand beyond the buffer underlying array
ciphertext, err := plaintext.EnsureSameSlice(ciphertext)
if err != nil {
    panic(fmt.Sprintf("Unexpected allocation performed during encryption: %v!", err))
}

// At this point, "ciphertext" and "plaintext" share the same underlying array, and are eventually the same buffer.

NB! Similar method accepting Buffer instead of slice is available: EnsureSameBuffer.

Recycle buffer

In the end, a buffer that was received from the pool (or any derivative of it) should be returned to the pool:

// Buffer will be efficiently recycled, runtime memory allocations will be minimized.
MessagePool.Put(messageBuf)

Putting it all together

This could be an example use case for Buffers:

import "crypto/cipher"

var MessagePool = betterbuf.CreateBufferPool(24, 128)

func readMessage() *Buffer {
    receivedBuf := MessagePool.GetFull()
    // TODO: read message into buffer slice (`receivedBuf.Slice()`)...
    return receivedBuf
}

func encryptMessage(buffer *Buffer) (*Buffer, err) {
    var nonce Buffer
    var aead cipher.AEAD
    // TODO: fill in cipher object and nonce...
    ciphertext := aead.Seal(plaintext.ResliceEnd(0), nonce.Slice(), plaintext.Slice(), nil)
    verified, err := plaintext.EnsureSameSlice(ciphertext)
    return verified, err
}

func sendMessage(buffer *Buffer) {
    var header Buffer
    // TODO: fill in message header for sending...
    messageBuf := buffer.PrependBuffer(header)
    // TODO: send message buffer slice (`receivedBuf.Slice()`)...
    MessagePool.Put(messageBuf)
}

func main() {
    plaintext := readMessage()
    ciphertext, err := encryptMessage(plaintext)
    if err == nil {
        sendMessage(ciphertext)
    } else {
        print("Unexpected error happened: %v", err)
        MessagePool.Put(plaintext)
    }
}

About

Handy buffers and pools for efficiant memory allocation management in GO

Topics

Resources

License

Stars

Watchers

Forks

Languages