firewood-ffi 0.3.1

C FFI bindings for Firewood, an embedded key-value store optimized for blockchain state.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE.md for licensing terms.

package ffi

// // Note that -lm is required on Linux but not on Mac.
// #include <stdlib.h>
// #include "firewood.h"
// #cgo noescape fwd_free_owned_bytes
// #cgo nocallback fwd_free_owned_bytes
// #cgo noescape fwd_free_owned_key_value_batch
// #cgo nocallback fwd_free_owned_key_value_batch
// #cgo noescape fwd_free_owned_kv_pair
// #cgo nocallback fwd_free_owned_kv_pair
import "C"

import (
	"errors"
	"fmt"
	"runtime"
	"unsafe"
)

var errFreeingValue = errors.New("unexpected error while freeing value")

// Borrower is an interface for types that can borrow or copy bytes returned
// from FFI methods.
type Borrower interface {
	// BorrowedBytes returns a slice of bytes that borrows the data from the
	// Borrower's internal memory.
	//
	// The returned slice is valid only as long as the Borrower is valid.
	// If the Borrower is freed, the slice will become invalid.
	BorrowedBytes() []byte

	// CopiedBytes returns a slice of bytes that is a copy of the Borrower's
	// internal memory.
	//
	// This is fully independent of the borrowed data and is valid even after
	// the Borrower is freed.
	CopiedBytes() []byte

	// Free releases the memory associated with the Borrower's data.
	//
	// It is safe to call this method multiple times. Subsequent calls will
	// do nothing if the data has already been freed (or was never set).
	//
	// However, it is not safe to call this method concurrently from multiple
	// goroutines. It is also not safe to call this method while there are
	// outstanding references to the slice returned by BorrowedBytes. Any
	// existing slices will become invalid and may cause undefined behavior
	// if used after the Free call.
	Free() error
}

var _ Borrower = (*ownedBytes)(nil)

// newBorrowedBytes creates a new BorrowedBytes from a Go byte slice.
//
// Provide a Pinner to ensure the memory is pinned while the BorrowedBytes is in use.
func newBorrowedBytes(slice []byte, pinner *runtime.Pinner) C.BorrowedBytes {
	// Get the pointer first to distinguish between nil slice and empty slice
	ptr := unsafe.SliceData(slice)
	sliceLen := len(slice)

	// If ptr is nil (which means the slice itself is nil), return nil pointer
	if ptr == nil {
		return C.BorrowedBytes{ptr: nil, len: 0}
	}

	// For non-nil slices (including empty slices like []byte{}),
	// pin the pointer if the slice has data
	if sliceLen > 0 {
		pinner.Pin(ptr)
	}

	return C.BorrowedBytes{
		ptr: (*C.uint8_t)(ptr),
		len: C.size_t(sliceLen),
	}
}

// newCBatchOp creates a new C.BatchOp from a Go BatchOp.
//
// Provide a Pinner to ensure the memory is pinned while the C.BatchOp is in use.
func newCBatchOp(op BatchOp, pinner *runtime.Pinner) C.BatchOp {
	var cOp C.BatchOp
	cOp.tag = op.tag
	switch op.tag {
	case C.BatchOp_Put:
		*(*C.BatchOp_Put_Body)(unsafe.Pointer(&cOp.anon0)) = C.BatchOp_Put_Body{
			key:   newBorrowedBytes(op.key, pinner),
			value: newBorrowedBytes(op.value, pinner),
		}
	case C.BatchOp_Delete:
		*(*C.BatchOp_Delete_Body)(unsafe.Pointer(&cOp.anon0)) = C.BatchOp_Delete_Body{
			key: newBorrowedBytes(op.key, pinner),
		}
	case C.BatchOp_DeleteRange:
		*(*C.BatchOp_DeleteRange_Body)(unsafe.Pointer(&cOp.anon0)) = C.BatchOp_DeleteRange_Body{
			prefix: newBorrowedBytes(op.key, pinner),
		}
	}
	return cOp
}

// newBorrowedBatchOps creates a new BorrowedBatchOps from a slice of C.BatchOp.
//
// Provide a Pinner to ensure the memory is pinned while the BorrowedBatchOps is
// in use.
func newBorrowedBatchOps(ops []C.BatchOp, pinner *runtime.Pinner) C.BorrowedBatchOps {
	sliceLen := len(ops)
	if sliceLen == 0 {
		return C.BorrowedBatchOps{ptr: nil, len: 0}
	}

	ptr := unsafe.SliceData(ops)
	if ptr == nil {
		return C.BorrowedBatchOps{ptr: nil, len: 0}
	}

	pinner.Pin(ptr)

	return C.BorrowedBatchOps{
		ptr: ptr,
		len: C.size_t(sliceLen),
	}
}

// newKeyValuePairsFromBatch creates a new BorrowedBatchOps from a slice of BatchOp.
//
// Provide a Pinner to ensure the memory is pinned while the BorrowedBatchOps is
// in use.
func newKeyValuePairsFromBatch(batch []BatchOp, pinner *runtime.Pinner) C.BorrowedBatchOps {
	if len(batch) == 0 {
		return C.BorrowedBatchOps{ptr: nil, len: 0}
	}
	ops := make([]C.BatchOp, len(batch))
	for i, op := range batch {
		ops[i] = newCBatchOp(op, pinner)
	}
	return newBorrowedBatchOps(ops, pinner)
}

// ownedBytes is a wrapper around C.OwnedBytes that provides a Go interface
// for Rust-owned byte slices.
//
// ownedBytes implements the [Borrower] interface allowing it to be shared
// outside of the FFI package without exposing the C types directly or any FFI
// implementation details.
type ownedBytes struct {
	owned C.OwnedBytes
}

// Free releases the memory associated with the Borrower's data.
//
// It is safe to call this method multiple times. Subsequent calls will
// do nothing if the data has already been freed (or was never set).
//
// However, it is not safe to call this method concurrently from multiple
// goroutines. It is also not safe to call this method while there are
// outstanding references to the slice returned by BorrowedBytes. Any
// existing slices will become invalid and may cause undefined behavior
// if used after the Free call.
func (b *ownedBytes) Free() error {
	if b.owned.ptr == nil {
		// Already freed (or never set), nothing to do.
		return nil
	}

	if err := getErrorFromVoidResult(C.fwd_free_owned_bytes(b.owned)); err != nil {
		return fmt.Errorf("%w: %w", errFreeingValue, err)
	}

	b.owned = C.OwnedBytes{}

	return nil
}

// BorrowedBytes returns the underlying byte slice. It may return nil if the
// data has already been freed was never set.
//
// The returned slice is valid only as long as the ownedBytes is valid.
//
// It does not copy the data; however, the slice is valid only as long as the
// ownedBytes is valid. If the ownedBytes is freed, the slice will
// become invalid.
//
// It is safe to cast the returned slice as a string so long as the ownedBytes
// is not freed while the string is in use.
//
// BorrowedBytes is part of the [Borrower] interface.
func (b *ownedBytes) BorrowedBytes() []byte {
	if b.owned.ptr == nil {
		return nil
	}

	return unsafe.Slice((*byte)(b.owned.ptr), b.owned.len)
}

// CopiedBytes returns a copy of the underlying byte slice. It may return nil if the
// data has already been freed or was never set.
//
// The returned slice is a copy of the data and is valid independently of the
// ownedBytes. It is safe to use after the ownedBytes is freed and will
// be freed by the Go garbage collector.
//
// CopiedBytes is part of the [Borrower] interface.
func (b *ownedBytes) CopiedBytes() []byte {
	if b.owned.ptr == nil {
		return nil
	}

	return C.GoBytes(unsafe.Pointer(b.owned.ptr), C.int(b.owned.len))
}

// intoError converts the ownedBytes into an error. This is used for methods
// that return a ownedBytes as an error type.
//
// If the ownedBytes is nil or has already been freed, it returns nil.
// Otherwise, the bytes will be copied into Go memory and converted into an
// error.
//
// The original ownedBytes will be freed after this operation and is no longer
// valid.
func (b *ownedBytes) intoError() error {
	if b.owned.ptr == nil {
		return nil
	}

	err := errors.New(string(b.CopiedBytes()))

	if err2 := b.Free(); err2 != nil {
		return fmt.Errorf("%w: %w (original error: %w)", errFreeingValue, err, err2)
	}

	return err
}

// newOwnedBytes creates a ownedBytes from a C.OwnedBytes.
//
// The caller is responsible for calling Free() on the returned ownedBytes
// when it is no longer needed otherwise memory will leak.
//
// It is not an error to provide an OwnedBytes with a nil pointer or zero length
// in which case the returned ownedBytes will be empty.
func newOwnedBytes(owned C.OwnedBytes) *ownedBytes {
	return &ownedBytes{owned: owned}
}

// getHashKeyFromHashResult creates a byte slice or error from a C.HashResult.
//
// It returns nil, nil if the result is None.
// It returns nil, err if the result is an error.
// It returns a byte slice, nil if the result is Some.
func getHashKeyFromHashResult(result C.HashResult) (Hash, error) {
	switch result.tag {
	case C.HashResult_NullHandlePointer:
		return EmptyRoot, errDBClosed
	case C.HashResult_None:
		return EmptyRoot, nil
	case C.HashResult_Some:
		cHashKey := (*C.HashKey)(unsafe.Pointer(&result.anon0))
		hashKey := *(*Hash)(unsafe.Pointer(&cHashKey._0))
		return hashKey, nil
	case C.HashResult_Err:
		ownedBytes := newOwnedBytes(*(*C.OwnedBytes)(unsafe.Pointer(&result.anon0)))
		return EmptyRoot, ownedBytes.intoError()
	default:
		return EmptyRoot, fmt.Errorf("unknown C.HashResult tag: %d", result.tag)
	}
}

// getErrorgetErrorFromVoidResult converts a C.VoidResult to an error.
//
// It will return nil if the result is Ok, otherwise it returns an error.
func getErrorFromVoidResult(result C.VoidResult) error {
	switch result.tag {
	case C.VoidResult_NullHandlePointer:
		return errDBClosed
	case C.VoidResult_Ok:
		return nil
	case C.VoidResult_Err:
		return newOwnedBytes(*(*C.OwnedBytes)(unsafe.Pointer(&result.anon0))).intoError()
	default:
		return fmt.Errorf("unknown C.VoidResult tag: %d", result.tag)
	}
}

// getValueFromValueResult converts a C.ValueResult to a byte slice or error.
//
// It returns nil, nil if the result is None.
// It returns nil, errRevisionNotFound if the result is RevisionNotFound.
// It returns a byte slice, nil if the result is Some.
// It returns an error if the result is an error.
func getValueFromValueResult(result C.ValueResult) ([]byte, error) {
	switch result.tag {
	case C.ValueResult_NullHandlePointer:
		return nil, errDBClosed
	case C.ValueResult_RevisionNotFound:
		// NOTE: the result value contains the provided root hash, we could use
		// it in the error message if needed.
		return nil, errRevisionNotFound
	case C.ValueResult_None:
		return nil, nil
	case C.ValueResult_Some:
		ownedBytes := newOwnedBytes(*(*C.OwnedBytes)(unsafe.Pointer(&result.anon0)))
		bytes := ownedBytes.CopiedBytes()
		if err := ownedBytes.Free(); err != nil {
			return nil, fmt.Errorf("%w: %w", errFreeingValue, err)
		}
		return bytes, nil
	case C.ValueResult_Err:
		err := newOwnedBytes(*(*C.OwnedBytes)(unsafe.Pointer(&result.anon0))).intoError()
		return nil, err
	default:
		return nil, fmt.Errorf("unknown C.ValueResult tag: %d", result.tag)
	}
}

type ownedKeyValueBatch struct {
	owned C.OwnedKeyValueBatch
}

func (b *ownedKeyValueBatch) copy() []*ownedKeyValue {
	if b.owned.ptr == nil {
		return nil
	}
	borrowed := b.borrow()
	copied := make([]*ownedKeyValue, len(borrowed))
	for i, borrow := range borrowed {
		copied[i] = newOwnedKeyValue(borrow)
	}
	return copied
}

func (b *ownedKeyValueBatch) borrow() []C.OwnedKeyValuePair {
	if b.owned.ptr == nil {
		return nil
	}

	return unsafe.Slice((*C.OwnedKeyValuePair)(unsafe.Pointer(b.owned.ptr)), b.owned.len)
}

func (b *ownedKeyValueBatch) free() error {
	if b == nil || b.owned.ptr == nil {
		// we want ownedKeyValueBatch to be typed-nil safe
		return nil
	}

	if err := getErrorFromVoidResult(C.fwd_free_owned_key_value_batch(b.owned)); err != nil {
		return fmt.Errorf("%w: %w", errFreeingValue, err)
	}

	b.owned = C.OwnedKeyValueBatch{}

	return nil
}

// newOwnedKeyValueBatch creates a ownedKeyValueBatch from a C.OwnedKeyValueBatch.
//
// The caller is responsible for calling Free() on the returned ownedKeyValue
// when it is no longer needed otherwise memory will leak.
func newOwnedKeyValueBatch(owned C.OwnedKeyValueBatch) *ownedKeyValueBatch {
	return &ownedKeyValueBatch{
		owned: owned,
	}
}

type ownedKeyValue struct {
	// owned holds the original C-provided pair so we can free it
	// with fwd_free_owned_kv_pair instead of freeing key/value separately.
	owned C.OwnedKeyValuePair
	// key and value wrappers provide Borrowed/Copied accessors
	key   *ownedBytes
	value *ownedBytes
}

func (kv *ownedKeyValue) copy() ([]byte, []byte) {
	key := kv.key.CopiedBytes()
	value := kv.value.CopiedBytes()
	return key, value
}

func (kv *ownedKeyValue) free() error {
	if kv == nil {
		// we want ownedKeyValue to be typed-nil safe
		return nil
	}
	if err := getErrorFromVoidResult(C.fwd_free_owned_kv_pair(kv.owned)); err != nil {
		return fmt.Errorf("%w: %w", errFreeingValue, err)
	}
	// zero out fields to avoid accidental reuse/double free
	kv.owned = C.OwnedKeyValuePair{}
	kv.key = nil
	kv.value = nil
	return nil
}

// newOwnedKeyValue creates a ownedKeyValue from a C.OwnedKeyValuePair.
//
// The caller is responsible for calling Free() on the returned ownedKeyValue
// when it is no longer needed otherwise memory will leak.
func newOwnedKeyValue(owned C.OwnedKeyValuePair) *ownedKeyValue {
	return &ownedKeyValue{
		owned: owned,
		key:   newOwnedBytes(owned.key),
		value: newOwnedBytes(owned.value),
	}
}

// getKeyValueFromResult converts a C.KeyValueResult to a key value pair or error.
//
// It returns nil, nil if the result is None.
// It returns a *ownedKeyValue, nil if the result is Some.
// It returns an error if the result is an error.
func getKeyValueFromResult(result C.KeyValueResult) (*ownedKeyValue, error) {
	switch result.tag {
	case C.KeyValueResult_NullHandlePointer:
		return nil, errDBClosed
	case C.KeyValueResult_None:
		return nil, nil
	case C.KeyValueResult_Some:
		ownedKvp := newOwnedKeyValue(*(*C.OwnedKeyValuePair)(unsafe.Pointer(&result.anon0)))
		return ownedKvp, nil
	case C.KeyValueResult_Err:
		err := newOwnedBytes(*(*C.OwnedBytes)(unsafe.Pointer(&result.anon0))).intoError()
		return nil, err
	default:
		return nil, fmt.Errorf("unknown C.KeyValueResult tag: %d", result.tag)
	}
}

// getKeyValueBatchFromResult converts a C.KeyValueBatchResult to a key value batch or error.
//
// It returns nil, nil if the result is None.
// It returns a *ownedKeyValueBatch, nil if the result is Some.
// It returns an error if the result is an error.
func getKeyValueBatchFromResult(result C.KeyValueBatchResult) (*ownedKeyValueBatch, error) {
	switch result.tag {
	case C.KeyValueBatchResult_NullHandlePointer:
		return nil, errDBClosed
	case C.KeyValueBatchResult_Some:
		ownedBatch := newOwnedKeyValueBatch(*(*C.OwnedKeyValueBatch)(unsafe.Pointer(&result.anon0)))
		return ownedBatch, nil
	case C.KeyValueBatchResult_Err:
		err := newOwnedBytes(*(*C.OwnedBytes)(unsafe.Pointer(&result.anon0))).intoError()
		return nil, err
	default:
		return nil, fmt.Errorf("unknown C.KeyValueBatchResult tag: %d", result.tag)
	}
}

// getDatabaseFromHandleResult converts a C.HandleResult to a Database or error.
//
// If the C.HandleResult is an error, it returns an error instead of a Database.
func getDatabaseFromHandleResult(result C.HandleResult) (*Database, error) {
	switch result.tag {
	case C.HandleResult_Ok:
		ptr := *(**C.DatabaseHandle)(unsafe.Pointer(&result.anon0))
		db := &Database{handle: ptr}
		return db, nil
	case C.HandleResult_Err:
		err := newOwnedBytes(*(*C.OwnedBytes)(unsafe.Pointer(&result.anon0))).intoError()
		return nil, err
	default:
		return nil, fmt.Errorf("unknown C.HandleResult tag: %d", result.tag)
	}
}

func newCHashKey(hash Hash) C.HashKey {
	return *(*C.HashKey)(unsafe.Pointer(&hash))
}