Awesome
Structured access to bytevector contents
Example
;; define a scheme bytestructure for a C struct
(define my-position-struct (bs:struct `((x ,int) (y ,int))))
;; initialize an instance of `my-position-struct`
(define position (bytestructure my-position-struct))
;; bytestructure-set!
(bytestructure-set! position 'x 42)
(bytestructure-set! position 'y 101)
;; bytestructure-ref
(bytestructure-ref position 'x) ;; => 42
;; retrieve the underlying bytevector
(define bv (bytestructure-bytevector position)) ;; => #vu8(42 0 0 0 101 0 0 0)
;; creating a bytestructure from an existing bytevector
(define position2 (make-bytestructure bv 0 my-position-struct))
There is also a faster macro API.
Introduction
This library offers a system imitating the type system of the C programming language, to be used on bytevectors. C's type system works on raw memory, and ours works on bytevectors which are an abstraction over raw memory in Scheme. The system is in fact more powerful than the C type system, elevating types to first-class status.
A C type corresponds to a "bytestructure descriptor" object in our system.
;; typedef uint8_t uint8_v3_t[3];
(define uint8-v3 (bs:vector 3 uint8))
;; typedef struct { uint16_t x; uint8_v3_t y; } my_struct_t;
(define my-struct (bs:struct `((x ,uint16) (y ,uint8-v3))))
These can then be bundled with a bytevector, yielding a "bytestructure" object on which referencing and assignment work in accordance with the types declared in the descriptor.
;; my_struct_t str;
(define str (bytestructure my-struct))
;; my_struct_t str = { 0, 1 };
(define str (bytestructure my-struct #(0 1)))
;; str.y[2]
(bytestructure-ref str 'y 2)
;; str.y[2] = 42;
(bytestructure-set! str 'y 2 42)
If your Scheme implementation supports syntax-case, then a macro-based API is available as well, for when the procedural API is too slow for your purposes.
(define-bytestructure-accessors my-struct
my-struct-unwrap my-struct-ref my-struct-set!)
(define foo (make-bytevector ...))
;; foo.y[2]
(my-struct-ref foo y 2)
;; foo.y[2] = 42;
(my-struct-set! foo y 2 42)
(Note that we don't use the bytestructure data type anymore; we work directly on bytevectors. The struct fields are also implicitly quoted and can't be variable references, since their look-up will happen at compile time. The unwrapper will be explained later.)
There are also "dynamic" bytestructure descriptors, whose behavior depends on the bytevector on which they're used. For instance a binary file format may specify that there are tag bytes declaring the lengths of following fields. The system can express this cleanly.
Supported platforms
R7RS and GNU Guile are supported. Detailed instructions per Scheme implementation follow.
Chibi
-
Clone the Larceny source repository:
git clone https://github.com/larcenists/larceny
-
Append
$larceny_repo/tools/R6RS
to the Chibi load-path via the-A
command-line flag. -
Append this directory to the Chibi load-path via the
-A
command-line flag. -
Import
(bytestructures r7)
.
Gauche
-
Clone the Larceny source repository:
git clone https://github.com/larcenists/larceny
-
Go to its
tools/R6RS/r6rs/
sub-directory. -
Run the following shell command in that directory and its sub-directories:
for file in *.sld; do name=${file%.sld} ln -s $file $name.scm done
-
Add
$larceny_repo/tools/R6RS
toGAUCHE_LOAD_PATH
. -
Add this directory to
GAUCHE_LOAD_PATH
. -
Import
(bytestructures r7)
.
Guile
- Add this directory to
GUILE_LOAD_PATH
.
(You can use the -L
command line flag instead of augmenting
GUILE_LOAD_PATH
, but don't use it with a relative path, because
include-from-path
doesn't work well with that, which we use.)
- Import
(bytestructures guile)
.
Kawa
-
Clone the Larceny source repository:
git clone https://github.com/larcenists/larceny
-
Run Kawa with a command line flag such as the following to add
$larceny_repo/tools/R6RS
and this directory to the load path, and to make it look for.sld
files:-Dkawa.import.path="$bytestructures_repo/*.sld:$larceny_repo/tools/R6RS/*.sld"
(The *
stands for any number of directories, so sub-directories will
also be searched for .sld
files.)
- Import
(bytestructures r7)
.
Larceny
-
Add this directory to
LARCENY_LIBPATH
. -
Run Larceny with the
-r7rs
flag. -
Import
(bytestructures r7)
.
Specification
A bytestructure descriptor, also called simply a descriptor within this specification, is an object encapsulating information about the layout and meanings of the bytes in a bytevector object.
A bytestructure is an object bundling a bytevector with a bytestructure descriptor so that values can be extracted from that bytevector conveniently, using the information in the descriptor.
A dynamic descriptor is a bytestructure descriptor whose size
and/or unwrapper
procedures reference their bytevector
and/or
offset
arguments. (See below.)
The argument name descriptor
signifies that an argument must be a
bytestructure descriptor, bytestructure
signifies that it must be a
bytestructure, and offset
signifies that it must be an exact
non-negative integer.
Knowledge of the C programming language is recommended for a proper understanding of this specification. Specifically, example code is often annotated with conceptually equivalent C code.
High-level API
A set of predefined bytestructure descriptors, as well as procedures for creating compound descriptors of certain kinds, are provided to the user, mostly obviating the need to work with the bytestructure descriptor API directly, which is explained further below.
Constructors for compound descriptors
bs:vector
(bs:vector length descriptor)
procedure
Returns a descriptor for vectors, also called a vector descriptor,
of length length
and the element descriptor descriptor
. This
corresponds to an array type in the C programming language.
;; uint16_t vec[3] = { 0, 1, 2 };
(define vec (bytestructure (bs:vector 3 uint16) #(0 1 2)))
;; vec[1]
(bytestructure-ref vec 1)
;; vec[1] = 42;
(bytestructure-set! vec 1 42)
The elements are indexed with exact non-negative integers, and no bounds checking is done; an off-bounds index will either raise an error due to an off-bounds bytevector index, or attempt to decode whatever bytes are found at the relevant place in the bytevector, which might result in a valid value without raising an error.
Vector descriptors are normally meant for indexing through, but also allow direct assignment. The value provided for assignment must be a regular Scheme vector of the same length as the vector descriptor. Each element of that vector is assigned to the corresponding element of the vector bytestructure, using the assignment semantics of the element descriptor.
;; (Reusing 'vec' from the previous example.)
;; Uses bytevector-u16-set! three times.
(bytestructure-set! vec #(21 42 84))
One may also provide a bytevector, in which case as many bytes as the size of the bytestructure will be copied into it.
;; The results of this depend on endianness.
;; Only the first 6 bytes from the bytevector will be copied.
(bytestructure-set! vec #u8(0 1 2 3 4 5 6 7 8))
These assignment semantics may not be used with the macro API.
Vector descriptors don't accept dynamic descriptors as their element descriptor, because they calculate their total size eagerly and thus need to know the size of their element descriptor independently from the bytevector on which they will be used.
bs:struct
(bs:struct fields)
procedure(bs:struct pack fields)
procedure
Returns a descriptor for structs, also called a struct descriptor.
Fields
must be a list of field specs (see below). Pack
may be
#f
, #t
, or an exact positive integer. If pack
is omitted or
#f
, the struct alignment of the platform's C ABI is used. If pack
is #t
, there are no padding fields (except for those resulting from
bit-fields). If pack
is an integer, it specifies the maximum
alignment value for the fields, similar to the #pack
directive of
the GCC C compiler.
A field spec is a list of two or three elements. The first element
must be a symbol which names the field (or #f
, see below). Every
field must have a distinct name (except if #f
). The second element
must be a bytestructure descriptor which becomes the descriptor of the
field. The third element, if present, must be an exact non-negative
integer; it signifies that the field is a bit-field of that width.
The descriptor of a bit-field must be one that decodes values to exact
integers, such as for instance uint8
or int32
.
Alternatively, a field spec may be of the form (union *fields*)
where fields is again a list of field specs. This feature
corresponds to anonymous unions in the C11 standard.
The width of a bit-field may be zero, which means padding should be
inserted in its place until the next alignment boundary of the
descriptor of that bit-field is reached. A zero-width bit-field must
have #f
as its name.
;; typedef struct { uint8_t x; uint16_t y; } my_struct_t;
(define my-struct (bs:struct `((x ,uint8) (y ,uint16))))
;; my_struct_t str = { 0, 1 };
(define str (bytestructure my-struct #(0 1)))
;; my_struct_t str = { .y = 1, .x = 0 };
(define str (bytestructure my-struct '((y 1) (x 0))))
;; str.y
(bytestructure-ref str 'y)
;; str.y = 42;
(bytestructure-set! str 'y 42)
;; Assuming a 32-bit platform:
;; struct { unsigned int a:16; unsigned int b:16; }
(bs:struct `((a ,uint32 16) (b ,uint32 16)))
;; struct { unsigned int a:16; int :0; signed int b:20; }
(bs:struct `((a ,uint32 16) (#f ,int32 0) (b ,int32 20)))
Struct descriptors are normally meant for indexing through, but also allow direct assignment. The value provided for assignment may be a Scheme vector as long as there are fields in the struct descriptor, which will assign all fields sequentially; or a list of two-element lists, which will assign any number of fields by name.
;; (Reusing 'str' from the previous example.)
;; str = (my_struct_t){ 0, 1 };
(bytestructure-set! str #(0 1))
;; str = (my_struct_t){ .y = 2, .x = 1 };
(bytestructure-set! str '((y 2) (x 1)))
One may also provide a bytevector, in which case as many bytes as the size of the bytestructure will be copied into it.
;; The field 'x' is set to 0; the value of the field 'y' will
;; depend on endianness.
;; Only the first 3 bytes from the bytevector will be copied.
(bytestructure-set! str #u8(0 1 2 3 4 5))
These assignment semantics may not be used with the macro API.
Struct descriptors don't accept dynamic descriptors as field descriptors, because they calculate their total size eagerly.
When using the macro API, the field names are implicitly quoted and looked up at macro-expand time.
(define-bytestructure-accessors my-struct
my-struct-unwrap my-struct-ref my-struct-set!)
;; foo.y
(my-struct-ref foo-bytevector y)
;; foo.y = 42;
(my-struct-set! foo-bytevector y 42)
bs:union
(bs:union fields)
procedure
Returns a descriptor for unions, also called a union descriptor.
Fields
has the same format as in bs:struct
.
;; typedef union { uint8_t x; uint16_t y; } my_union_t;
(define my-union (bs:union `((x ,uint8) (y ,uint16))))
;; my_union_t union = { .y = 42 };
(define union (bytestructure my-union '(y 42)))
;; union.y
(bytestructure-ref union 'y)
;; union.y = 42;
(bytestructure-set! union 'y 42)
Union descriptors are normally meant for indexing through, but also allow direct assignment. The value provided for assignment must be a two-element list, whose first element names the field whose descriptor should be used for the assignment, and the second element provides the value to be actually assigned.
;; union.y = 42;
(bytestructure-set! union '(y 42))
Rationale: This syntax isn't shorter than the normal way of
assigning a value into the union, but is supported for reasons that
should become apparent after reading the specification of the
bytestructure
constructor procedure.
One may also provide a bytevector, in which case as many bytes as the size of the bytestructure will be copied into it.
;; The value of the y field will depend on endianness.
;; Only the first 2 bytes from the bytevector will be copied.
(bytestructure-set! union #u8(0 1 2 3 4))
These assignment semantics may not be used with the macro API.
Union descriptors don't accept dynamic descriptors as field descriptors, because they calculate their total size eagerly.
bs:pointer
(bs:pointer descriptor)
procedure
Returns a descriptor for pointers, also called a pointer descriptor,
with the content descriptor descriptor
. Such a descriptor
indicates that the bytes in a given bytevector are to be interpreted
as a memory address. The content descriptor is the descriptor for the
bytes found at that memory address.
;; foo_struct *ptr = 0x12345678;
(define ptr (bytestructure (bs:pointer foo-struct) #x12345678))
For void pointers, the symbol void
may be used in place of a content
descriptor:
;; void *ptr;
(define ptr (bytestructure (bs:pointer 'void)))
As a special case, the descriptor
argument to bs:pointer
may be a
promise, which must evaluate to a descriptor when forced. This is to
allow creating self-referencing descriptors:
;; typedef struct linked_uint8_list_s {
;; uint8_t head;
;; struct linked_uint8_list_s *tail;
;; } *linked_uint8_list_t;
(define linked-uint8-list
(bs:pointer (delay (bs:struct `((head ,uint8)
(tail ,linked-uint8-list))))))
The symbol *
can be used as an index to dereference the pointer.
(It's implicitly quoted when used in the macro API.) An array of
bytes as large as the size of the content descriptor, starting from
the memory address of the pointer, are reified into a bytevector
object, and bundled with the content descriptor, to yield a new
bytestructure object.
;; linked_uint8_list_t u8list;
(define u8list (bytestructure linked-uint8-list))
;; u8list->head
(bytestructure-ref u8list '* 'head)
;; u8list->head = 42;
(bytestructure-set! u8list '* 'head 42)
One may also provide an integer index, which will result in an offset being applied while creating the reified bytevector:
;; uint8_t *u8array;
(define u8array (bytestructure (bs:pointer uint8)))
;; u8array[5]
(bytestructure-ref u8array 5)
;; u8array[5] = 42
(bytestructure-set! u8array 5 42)
Note: Since dereferencing a pointer involves the creation of a new bytevector object, it's a rather inefficient operation relative to what it achieves (following a pointer). As such, you might want to minimize the use of pointer dereferences in performance-critical sections of code. Note that using the macro API does not work around this issue, as the bytevector still needs to be created at run-time to access the data referenced by the pointer. To minimize the number of bytevector allocations, create one explicitly with the appropriate size. Example with Guile:
(import (bytestructures guile))
(import (rnrs bytevectors))
(import (prefix (system foreign) ffi:))
;; struct string { size_t len; char *chars; }
(define string-descriptor
(bs:struct `((len ,size_t) (chars ,(bs:pointer uint8)))))
;; struct string my_string;
(define my-string (bytestructure string-descriptor))
;; my_string.len = 30;
(bytestructure-set! my-string 'len 30)
;; my_string.chars = malloc(30);
(bytestructure-set! my-string 'chars (make-bytevector 30))
;; Let's hope the bytevector doesn't get garbage collected!
;; Slow code to take the sum of all chars:
(let ((len (bytestructure-ref my-string 'len)))
(do ((i 0 (+ i 1))
(sum 0 (+ sum (bytestructure-ref my-string 'chars i))))
((= i len) sum)))
;; Fast code:
(let* ((len (bytestructure-ref my-string 'len))
(addr (bytestructure-ref my-string 'chars))
(pointer (ffi:make-pointer addr))
(bv (ffi:pointer->bytevector pointer len)))
(do ((i 0 (+ i 1))
(sum 0 (+ sum (bytevector-u8-ref bv i))))
((= i len) sum)))
;; Or continue with bytestructures after following the pointer:
(let* ((len (bytestructure-ref my-string 'len))
(addr (bytestructure-ref my-string 'chars))
(pointer (ffi:make-pointer addr))
(bv (ffi:pointer->bytevector pointer len))
(chars (make-bytestructure bv 0 (bs:vector len uint8))))
(do ((i 0 (+ i 1))
(sum 0 (+ sum (bytestructure-ref chars i))))
((= i len) sum)))
Since pointers are also values themselves, pointer descriptors also have direct referencing and assignment semantics. Referencing the pointer yields the numeric value of the address.
;; linked_uint8_list_t u8lists[3];
(define u8lists (bytestructure (bs:vector 3 linked-uint8-list)))
;; Returns the address stored in u8lists[1].
(bytestructure-ref u8lists 1)
Assignment with a pointer descriptor allows a variety of values. Firstly, a numeric value (taken to be a memory address) may be given, which causes that value itself to be written.
;; uint8_t (*u8v3-ptr)[3];
(define u8v3-ptr (bytestructure (bs:pointer (bs:vector 3 uint8))))
;; u8v3-ptr = 0xdeadbeef;
(bytestructure-set! u8v3-ptr #xdeadbeef)
A bytevector may be given, in which case the memory address of the first byte of the bytevector is written.
;; Makes the pointer point to 'a-bytevector'.
(bytestructure-set! u8v3-ptr a-bytevector)
Lastly, providing a bytestructure is equivalent to providing the bytevector of that bytestructure.
;; Makes the pointer point to the bytevector of 'a-bytestructure'.
(bytestructure-set! u8v3-ptr a-bytestructure)
These assignment semantics may be used with the macro API as well.
Pointers don't accept dynamic descriptors as their content descriptor.
Rationale: The bytevector that is pointed to is reified "on the fly" during referencing operations, for which its size needs to be known in advance. Needing the bytevector to already exist for calculating its size (as is the case for dynamic descriptors) imposes a problem of circularity.
Note: Having an address written into a bytevector may not protect it from garbage collection. Thus using pointer descriptors might make a Scheme program memory unsafe even if the Scheme implementation is otherwise memory safe.
Numeric descriptors
The following descriptors for numeric types are provided:
[u]int(8,16,32,64)[le,be]
, float(32,64)[le,be]
,
complex(64,128)[le,be]
On platforms with little-endian byte order, the descriptors whose name
ends in le
are equivalent as per eqv?
to their variant without an
explicit endianness marker. The same applies for the big-endian
descriptors on big-endian platforms.
The following are each equivalent as per eqv?
to one of the above
listed descriptors, depending on the platform on which the Scheme
program is run: [unsigned-](short,int,long,long-long)
,
[u]intptr_t
, [s]size_t
, ptrdiff_t
, float
, double
These descriptors cannot be indexed through as for instance vectors and structs can; they can only be used to directly reference or assign values.
;; uint32_t x;
(define x (bytestructure uint32))
;; x = 42;
(bytestructure-set! x 42)
;; uint32_t xs[3];
(define xs (bytestructure (bs:vector 3 uint32)))
;; xs[1] = 42;
(bytestructure-set! xs 1 42)
String descriptors
(bs:string size encoding)
procedure
Returns a descriptor for a string occupying size
bytes, encoded in
encoding
(a symbol). Currently supported encodings:
ascii
utf8
utf16le
utf16be
utf32le
utf32be
If the ASCII encoding is specified, an error is raised if a non-ASCII character is encountered during encoding or decoding.
Byte-order marks are not supported (yet).
(define x (bytestructure (bs:string 8 'utf16le)))
(bytestructure-set! x "1234")
(bytestructure-ref x) ;=> "1234"
When writing a string into a bytevector via such a descriptor, the given string must fit into the given size after encoding, otherwise an error is raised. If the string is shorter than the size, the remaining bytes of the bytevector are zeroed if the specified encoding is variable-width (UTF-8 and UTF-16), otherwise an error is raised.
(define x (bytestructure (bs:string 4 'utf8)))
(bytestructure-set! x "12345") ;error
(bytestructure-set! x "123")
(bytestructure-ref x) ;=> "123\x00"
Null-terminated C strings
Currently only supported on Guile.
The cstring-pointer
descriptor can be used to represent a pointer to
a null-terminated string. A reference operation will return that
string as a Scheme string. The setter only takes addresses to
existing C strings however, due to the difficulty of holding a
reference to the associated pointer object in Scheme.
(import (prefix (system foreign) ffi:)) ;use Guile FFI module
(define bs (bytestructure cstring-pointer))
;; This creates a null-terminated string "foobar\0" in memory, giving
;; us a pointer object holding its address.
(define ptr (ffi:string->pointer "foobar"))
;; Write the address of "foobar\0" into the backing bytevector.
(bytestructure-set! bs (ffi:pointer-address ptr))
;; Get the null-terminated string whose address is found in the
;; backing bytevector.
(bytestructure-ref bs) ;=> "foobar"
The bytestructure data type
(make-bytestructure bytevector offset descriptor)
procedure
Returns a bytestructure object with the given bytevector, offset into the bytevector, and bytestructure descriptor.
Rationale: Any bytestructure descriptor can be used with any bytevector to work on it momentarily in accordance with the descriptor, but in most cases a bytevector is dedicated to a certain structure, so it makes sense to bundle a descriptor with the bytevector. Or only a portion of the bytevector, starting from a certain offset, might be dedicated to the structure, so being able to bundle that offset is also useful.
(bytestructure? obj)
procedure
Returns a Boolean indicating whether obj
is a bytestructure.
(bytestructure-bytevector bytestructure)
procedure(bytestructure-offset bytestructure)
procedure(bytestructure-descriptor bytestructure)
procedure
These procedures return the bytevector
, offset
, and descriptor
values respectively, with which bytestructure
was created.
(bytestructure-size bytestructure)
procedure
Returns the size of the structure contained within bytestructure
.
(bytestructure descriptor)
procedure(bytestructure descriptor initial-value)
procedure
Creates a bytestructure with a newly allocated bytevector of the right
size for descriptor
and an offset of 0, and optionally initializes
it with values.
The following two expressions are equivalent:
(define bs (bytestructure descriptor))
(define bs (make-bytestructure
(make-bytevector (bytestructure-descriptor-size
descriptor))
0
descriptor))
The optional second argument is passed to bytestructure-set!
to
assign the given values to the bytestructure after creation, meaning
the following two expressions are equivalent:
(define bs (bytestructure descriptor values))
(let ((bs (bytestructure descriptor)))
(bytestructure-set! bs values)
bs)
Since the setter procedures of compound descriptors tend to delegate the assignment of individual elements to their respective descriptors, one can easily initialize structures to arbitrary depth.
(define my-struct
(bs:struct `((x ,uint16) (y ,(bs:vector 3 uint8)))))
(define bs (bytestructure my-struct '((x 0) (y #(0 1 2)))))
Referencing and assignment
(bytestructure-ref bytestructure index ...)
syntax
Traverses through bytestructure
using bytestructure-unwrap
with
the given indices to acquire a triple of a bytevector, offset, and
descriptor. Then, applies the getter
of that descriptor to the
bytevector and offset. Or if the getter is #f
, then a bytestructure
encapsulating that bytevector, offset, and descriptor is returned.
Note that this means that calling bytestructure-ref
with zero index
arguments will return a bytestructure identical to the one provided.
(bytestructure-set! bytestructure index ... value)
syntax
Traverses through bytestructure
using bytestructure-unwrap
with
the given indices to acquire a triple of a bytevector, offset, and
descriptor. Then, applies the setter
of that descriptor to the
bytevector, offset, and value
. Or if the setter is #f
, then
value
must be a bytevector; as many bytes as the size of the
descriptor are copied from it into the bytevector, starting from the
offset.
(bytestructure-ref* bytevector offset descriptor index ...)
syntax(bytestructure-set!* bytevector offset descriptor index ... value)
syntax
These macros have the same semantics as bytestructure-ref
and
bytestructure-set!
respectively, except that they start the
referencing process with the given bytevector
, offset
, and
descriptor
, instead of the bytevector, offset, and descriptor of a
given bytestructure.
(bytestructure-unwrap bytestructure index ...)
syntax
This macro executes the following algorithm:
-
Extract the bytevector, offset, and descriptor of
bytestructure
. Let us call the triple of these values the working set. -
If no indices are left, return the working set as three values.
-
Apply the
unwrapper
procedure of the descriptor to the bytevector, the offset, and the first index. The return values replace the working set. Pop the index from the list of indices. -
Go to step 2.
Note: bytestructure-unwrap
can be used with zero indices to
destructure a bytestructure into its contents.
(let-values (((bytevector offset descriptor)
(bytestructure-unwrap bytestructure)))
...)
(bytestructure-unwrap* bytevector offset descriptor index ...)
syntax
This macro has the same semantics as bytestructure-unwrap
, except
that it starts the traversal process with the given bytevector
,
offset
, and descriptor
, instead of the bytevector, offset, and
descriptor of a given bytestructure.
When a descriptor is not a dynamic descriptor, bytestructure-unwrap*
may be given a bogus bytevector
argument.
(bytestructure-unwrap* #f 0 uint8-v3-v5 2)
=> #f, 6, uint8-v3 ;; Two uint8-v3s were skipped, so offset 6.
(bytestructure-unwrap* #f 0 uint8-v3-v5 2 1)
=> #f, 7, uint8 ;; Two uint8-v3s and one uint8 was skipped.
(bytestructure-ref/dynamic bytestructure index ...)
procedure(bytestructure-set!/dynamic bytestructure index ... value)
procedure
These procedures are equivalent to the macros bytestructure-ref
and
bytestructure-set!
respectively.
Rationale: Since these procedures take a variable number of arguments, they have to allocate rest-arguments lists, which might be undesirable in the general case.
Macro-based API
For when maximal efficiency is desired, a macro-based API is offered, so that the bulk of the work involved in offset calculation can be offloaded to the macro-expand phase.
(define-bytestructure-accessors descriptor unwrapper getter setter getter* setter*)
syntax
The descriptor
expression is evaluated during the macro-expand phase
to yield a bytestructure descriptor. The unwrapper
, getter
, and
setter
identifiers are bound to a triple of macros implementing the
indexing, referencing, and assignment semantics of the descriptor.
The getter*
and setter*
variants allow an initial offset argument,
whereas the plain variants implicitly use 0 as the base offset.
(define-bytestructure-accessors (bs:vector 5 (bs:vector 3 uint8))
uint8-v3-v5-unwrap
uint8-v3-v5-ref
uint8-v3-v5-set!
uint8-v3-v5-ref*
uint8-v3-v5-set!*)
(uint8-v3-v5-unwrap #f 0 3 2) ;the #f is a bogus bytevector
;the 0 is the initial offset
=> 11 (3 * 3 + 2)
;; bv = uint8_t[15]{ 0, 1, 2, ... 14 };
(define bv (apply bytevector (iota 15)))
;; ((uint8_t[5][3]) bv)[2][1]
(uint8-v3-v5-ref bv 2 1) => 7
;; ((uint8_t[5][3]) bv)[2][1] = 42
(uint8-v3-v5-set! bv 2 1 42)
;; ((uint8_t[5][3]) bv)[2][1]
(uint8-v3-v5-ref bv 2 1) => 42
;; bv2 = uint8_t[20]{ 0, 1, 2, ... 19 };
(define bv2 (apply bytevector (iota 20)))
;; ((uint8_t[5][3]) (bv2 + 5))[2][1]
(uint8-v3-v5-ref* bv2 5 2 1) => 12
;; ((uint8_t[5][3]) (bv2 + 5))[2][1] = 42
(uint8-v3-v5-set!* bv2 5 2 1 42)
;; ((uint8_t[5][3]) (bv2 + 5))[2][1]
(uint8-v3-v5-ref* bv2 5 2 1) => 42
The macro API internally uses the following procedures during the macro-expand phase to generate the desired output syntax:
(bytestructure-unwrap/syntax bytevector-syntax offset-syntax descriptor indices-syntax)
procedure
The semantics are akin to bytestructure-unwrap*
, except that some
arguments are syntax objects, and the return value is a syntax object
that would evaluate to two values: the bytevector and offset that are
the result of the indexing process.
(bytestructure-ref/syntax bytevector-syntax offset-syntax descriptor indices-syntax)
procedure
The semantics are akin to bytestructure-ref*
, except that some
arguments are syntax objects, and the return value is a syntax object
that would evaluate to the decoded value.
(bytestructure-set!/syntax bytevector offset descriptor indices value)
procedure
The semantics are akin to bytestructure-set!*
, except that some
arguments are syntax objects, and a syntax object is returned that
would perform the actual assignment when evaluated.
The bytestructure descriptors API
(make-bytestructure-descriptor size alignment unwrapper getter setter)
procedure
Size
must be an exact non-negative integer, or a procedure taking
three arguments and returning an exact non-negative integer (this is
for dynamic descriptors). The first argument to the procedure is a
Boolean indicating whether the call to the procedure is happening in
the macro-expand phase. If it's false, the other two arguments are a
bytevector and an offset into the bytevector respectively. If it's
true, then the two arguments are instead syntax objects that would
evaluate to a bytevector and an offset respectively. The offset is
the position in the bytevector at which the bytes belonging to the
descriptor start. The procedure should return the size of the
structure described by the descriptor, or return a syntax object that
would evaluate to the size.
Alignment
must be an exact positive integer specifying the type's
preferred memory alignment.
Unwrapper
must be #f
or a procedure taking four arguments: a
Boolean indicating whether the call to the procedure is happening in
the macro-expand phase, a bytevector (or syntax object thereof), an
offset (or syntax object thereof), and an index object (or syntax
object thereof). The procedure must return three values: the same or
another bytevector (or syntax object thereof), a new offset (or syntax
object thereof), and a bytestructure descriptor (NOT a syntax object
thereof). This procedure implements the indexing semantics of
compound types. The bytevector argument is provided to satisfy
dynamic descriptors; the unwrapper
of non-dynamic descriptors should
ignore its value and return it back untouched.
Getter
must be #f
or a procedure taking three arguments: a Boolean
indicating whether the call to the procedure is happening in the
macro-expand phase, a bytevector (or syntax object thereof), and an
offset (or syntax object thereof). The procedure should decode the
bytes at the given offset in the given bytevector (or return a syntax
object whose evaluation would do this), thus implementing the
referencing semantics of the descriptor.
Setter
must be #f
or a procedure taking four arguments: a Boolean
indicating whether the call to the procedure is happening in the
macro-expand phase, a bytevector (or syntax object thereof), an offset
(or syntax object thereof), and a value (or syntax object thereof).
The procedure should encode the given value into given offset in the
given bytevector (or return a syntax object whose evaluation would do
this), thus implementing the assignment semantics of the descriptor.
(bytestructure-descriptor-size descriptor)
procedure(bytestructure-descriptor-size descriptor bytevector offset)
procedure
Returns the size of descriptor
. If descriptor
is a dynamic
descriptor, then the bytevector
and offset
arguments must be
provided, which will be passed to the size
procedure of
descriptor
, with the macro-expand Boolean argument set to false.
(bytestructure-descriptor-size uint8-v3-v5)
=> 15, because 3×5 8-bit integers in total.
(bytestructure-descriptor-size a-dynamic-descriptor)
;;; error
(bytestructure-descriptor-size
a-dynamic-descriptor bytevector offset)
=> 42
(bytestructure-descriptor-size/syntax descriptor)
procedure(bytestructure-descriptor-size/syntax descriptor bytevector-syntax offset-syntax)
procedure
Returns a syntax object that would evaluate to the size of
descriptor
. If descriptor
is a dynamic descriptor, then the
bytevector-syntax
and offset-syntax
arguments must be provided,
which will be passed to the size
procedure of descriptor
, with the
macro-expand Boolean argument set to true.
(bytestructure-descriptor-alignment descriptor)
procedure(bytestructure-descriptor-unwrapper descriptor)
procedure(bytestructure-descriptor-getter descriptor)
procedure(bytestructure-descriptor-setter descriptor)
procedure
These procedures return the alignment
, unwrapper
, getter
, and
setter
values respectively, with which descriptor
was created.
Performance
Macro API
The macro API incurs zero run-time overhead for normal referencing and assignment operations, since most things happen in the macro-expand phase.
Plain bytevector reference:
> (define times (iota 1000000)) ;A million
> (define bv (make-bytevector 1))
> (define (ref x) (bytevector-u8-ref bv 0))
> ,time (for-each ref times)
;; ~0.14s real time
Bytestructure reference:
> (define bv (make-bytevector 1000))
> (define-bytestructure-accessors
(bs:vector 5 (bs:vector 5 (bs:struct `((x ,uint8)
(y ,uint8)
(z ,uint8)))))
bs-unwrap bs-ref bs-set!)
> (define (ref x) (bs-ref bv 4 4 z))
> ,time (for-each ref times)
;; ~0.14s real time
(Ignoring the jitter for both.)
Procedural API
When descriptors are statically apparent, an aggressively constant propagating and partial evaluating optimizer might be able to turn bytestructure references into direct bytevector references, yielding identical results to the macro API. That is the most optimal outcome, but more realistic is that most of the work happens at run-time.
The offset calculation avoids allocation, which will make its speed predictable. It takes linear time with regard to the depth of a structure. For structs and unions, it's also linear with regard to the position of the referenced field, but the constant factor involved in that is so small that this should usually not be noticed unless you have a very large number of struct or union fields.
If performance becomes an issue but you can't or don't want to switch
to the macro API, you can improve performance by hoisting as much work
to outside of your tight loops or other performance critical sections
of your code. E.g. if you were doing (bytestructure-ref bs x y z)
within a loop, you can instead do
(let-values (((bytevector offset descriptor)
(bytestructure-unwrap bs x y z)))
(loop
(bytestructure-ref* bytevector offset descriptor)))
or if for instance the last index in that example, z
, changes at
every iteration of the loop, you can do
(let-values (((bytevector offset descriptor)
(bytestructure-unwrap bs x y)))
(loop (for z in blah)
(bytestructure-ref* bytevector offset descriptor z)))
so at least you don't repeat the indexing of x
and y
at every
iteration.
Following are some benchmark figures from Guile 2.2.2 on an Intel i5. (These are only meant for a broad comparison against plain bytevector reference.)
Prelude:
(import
(bytestructures guile)
(rnrs bytevectors))
(define million-times (iota 1000000))
Plain bytevector reference:
(define bv (make-bytevector 1))
(define (ref x) (bytevector-u8-ref bv 0))
,time (for-each ref million-times)
;; ~0.06s real time
Equivalent bytestructure reference:
(define bs (bytestructure (bs:vector 1 uint8)))
(define (ref x) (bytestructure-ref bs 0))
,time (for-each ref million-times)
;; ~0.35s real time (5.8 times of plain bytevector ref)
Showcasing the effect of a deeper structure:
(define bs (bytestructure (bs:vector 1
(bs:vector 1
(bs:vector 1 uint8)))))
(define (ref x) (bytestructure-ref bs 0 0 0))
,time (for-each ref million-times)
;; ~0.59s real time (9.8 times of plain bytevector ref)