Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

defstruct

defstruct declares a C-compatible struct layout. The compiler generates accessors for both the BPF side (inside defprog) and the Common Lisp userspace side (for packing and unpacking data exchanged through maps and ring buffers).

Syntax

(defstruct name
  (field-name type)
  ...)

Field Types

TypeSizeDescription
u81 byteUnsigned 8-bit integer
u162 bytesUnsigned 16-bit integer
u324 bytesUnsigned 32-bit integer
u648 bytesUnsigned 64-bit integer
(array type count)sizeof(type) * countFixed-size array of a scalar type

Layout and Alignment

Fields are laid out with C-compatible natural alignment. Each field is aligned to its own size: u16 fields align to 2 bytes, u32 to 4 bytes, u64 to 8 bytes. The compiler inserts padding bytes between fields as needed. The overall struct size is padded to a multiple of the largest field alignment.

For example:

(defstruct sample
  (flags u8)
  (id u32)
  (value u64))

This produces a 16-byte struct: 1 byte for flags, 3 bytes of padding, 4 bytes for id, and 8 bytes for value.

BPF-Side API

Inside a defprog body, defstruct generates the following forms:

Constructor

(make-NAME)

Allocates the struct on the BPF stack with all fields zeroed.

(let ((e (make-sample)))
  (setf (sample-id e) 42)
  ...)

Field Accessor

(NAME-FIELD ptr)

Reads a field value from a struct pointer.

(let ((pid (sample-id e)))
  ...)

Field Setter

(setf (NAME-FIELD ptr) value)

Writes a value to a field.

(setf (sample-value e) 100)

Field Pointer

(NAME-FIELD-PTR ptr)

Returns a pointer to the field within the struct. This is useful for passing field addresses to BPF helpers like bpf_probe_read or bpf_get_current_comm:

(get-current-comm (my-event-comm-ptr e) 16)

Sizeof

(sizeof NAME)

Returns the total size of the struct in bytes, including padding. This is typically used with ringbuf-reserve:

(ringbuf-reserve events (sizeof my-event))

CL-Side API

On the Common Lisp userspace side, defstruct generates:

Record Struct

NAME-RECORD

A standard CL record type with a slot for each field. Array fields become CL vectors.

Decoder

(decode-NAME byte-vector &optional offset)

Parses a byte vector (or a subrange starting at offset) into a NAME-RECORD instance. This is used when reading data from maps or ring buffers.

Encoder

(encode-NAME record)

Serializes a NAME instance into a byte vector suitable for writing to a map.

Full Example

Consider a struct for reporting process events:

(defstruct my-event
  (pid u32)
  (comm (array u8 16))
  (data (array u8 64)))

This defines a struct with a 32-bit PID, a 16-byte command name buffer, and a 64-byte data buffer. The total size is 84 bytes (4 + 16 + 64, no padding needed since the largest field alignment is 4).

BPF side

(defmap events :type :ringbuf
  :max-entries (* 256 1024))

(defprog trace-exec (:type :tracepoint
                     :section "tracepoint/sched/sched_process_exec")
  (with-ringbuf (e events (sizeof my-event))
    (setf (my-event-pid e) (get-current-pid-tgid))
    (get-current-comm (my-event-comm-ptr e) 16)
    (probe-read (my-event-data-ptr e) 64 some-source))
  0)

CL side

;; Reading events from the ring buffer
(with-ringbuf-consumer (buf events)
  (lambda (data size)
    (let ((evt (decode-my-event data)))
      (format t "pid=~A comm=~A~%"
              (my-event-record-pid evt)
              (map 'string #'code-char
                   (my-event-record-comm evt))))))

;; Creating and encoding a record manually
(let ((rec (make-my-event-record :pid 1234
                                 :comm (make-array 16 :element-type '(unsigned-byte 8))
                                 :data (make-array 64 :element-type '(unsigned-byte 8)))))
  (encode-my-event rec))