@obelusfi/bun-cstruct 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +267 -0
  2. package/index.ts +376 -0
  3. package/package.json +33 -0
package/README.md ADDED
@@ -0,0 +1,267 @@
1
+ # @obelusfi/bun-cstruct
2
+
3
+ A small convenience layer over `bun:ffi` for working with C structs using TypeScript classes and decorators.
4
+
5
+ It lets you describe C memory layouts declaratively and access native memory through strongly typed class instances — without manually calculating offsets.
6
+
7
+ This library exists as a practical solution while waiting for an official Bun API for structured C memory access.
8
+
9
+ ## Features
10
+
11
+ - Declarative struct definitions via decorators
12
+ - Deterministic C-compatible memory layout
13
+ - Nested inline structs
14
+ - Pointer-backed struct references
15
+ - Fixed-size arrays
16
+ - Fixed-size inline strings
17
+ - C string (`char*`) support
18
+ - Enumerable fields (works with `Object.keys`)
19
+ - JSON serialization support
20
+ - Manual struct allocation
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ bun add @obelusfi/bun-cstruct
26
+ ```
27
+
28
+ add `"experimentalDecorators": true` to your `tsconfig.json`
29
+
30
+ ## Basic Usage
31
+
32
+ ### Define Structs
33
+
34
+ ```ts
35
+ import {
36
+ CStruct,
37
+ i32,
38
+ u16,
39
+ f32,
40
+ chars,
41
+ array,
42
+ struct,
43
+ ref,
44
+ refPointer,
45
+ string,
46
+ } from "@obelusfi/bun-cstruct";
47
+
48
+ import type { Pointer } from "bun:ffi";
49
+
50
+ class Ref extends CStruct {
51
+ @i32 prop!: number;
52
+ @f32 pi!: number;
53
+ @string greeting!: string; // char*
54
+ }
55
+
56
+ class Nested extends CStruct {
57
+ @i32 z!: number;
58
+ @chars(5) text!: string; // inline char[5]
59
+ @array(i32, 3) list!: number[]; // inline int[3]
60
+ @ref(Ref) someReference!: Ref; // Ref*
61
+ }
62
+
63
+ class SomeStruct extends CStruct {
64
+ @i32 a!: number;
65
+ @u16 b!: number;
66
+ @struct(Nested) child!: Nested; // inline struct
67
+ @ref(Ref) someReference!: Ref; // Ref*
68
+
69
+ // Pointer to the ref is available via `$` prefix
70
+ @refPointer $someReference!: Pointer;
71
+ }
72
+ ```
73
+
74
+ ### Load a Native Library
75
+
76
+ ```ts
77
+ import { dlopen, type Pointer } from "bun:ffi";
78
+
79
+ const { symbols: lib, close } = dlopen("./libexample.dylib", {
80
+ doSomethingWithStruct: {
81
+ args: ["pointer"],
82
+ },
83
+ getStruct: {
84
+ returns: "pointer",
85
+ },
86
+ });
87
+ ```
88
+
89
+ ### Use a Struct
90
+
91
+ ```ts
92
+ const ptr: Pointer = lib.getStruct();
93
+ const s = new SomeStruct(ptr);
94
+
95
+ console.log(s.a); // read from native memory
96
+ s.a = 10; // write to native memory
97
+
98
+ console.log(s.child.z); // nested struct
99
+ console.log(s.child.list[0]); // inline array
100
+
101
+ s.child.text = "hello world"; // safely truncated
102
+ ```
103
+
104
+ ### Allocate a Struct
105
+
106
+ The static method `alloc(): Pointer` allows you to allocate memory for a struct using its computed size.
107
+
108
+ ```ts
109
+ const ptr: Pointer = SomeStruct.alloc();
110
+ lib.doSomethingWithStruct(ptr);
111
+ ```
112
+
113
+ ### Get size of struct
114
+
115
+ Extending `CStruct` adds a static prop `size` to your class
116
+
117
+ ```ts
118
+ SomeStruct.size; // in bytes
119
+ ```
120
+
121
+ ## Supported Field Decorators
122
+
123
+ ### Primitives
124
+
125
+ - `@i8`
126
+ - `@i16`
127
+ - `@i32`
128
+ - `@i64`
129
+ - `@u8`
130
+ - `@u16`
131
+ - `@u32`
132
+ - `@u64`
133
+ - `@f32`
134
+ - `@f64`
135
+ - `@intptr`
136
+ - `@ptr`
137
+
138
+ ### Inline Fixed-Size String
139
+
140
+ ```ts
141
+ {
142
+ // ...
143
+ @chars(10) name!: string;
144
+ // ...
145
+ }
146
+ ```
147
+
148
+ - Stored as `char[10]`
149
+ - Truncates safely on overflow
150
+
151
+ ### C String (`char*`)
152
+
153
+ ```ts
154
+ {
155
+ // ....
156
+ @string label!: string;
157
+ // ....
158
+ }
159
+ ```
160
+
161
+ - Reads/writes null-terminated strings via pointer
162
+ - ⚠️ Writes do **not** free the overwritten pointer (memory management is your responsibility)
163
+
164
+ ### Inline Array
165
+
166
+ ```ts
167
+ {
168
+ //...
169
+ @array(i32, 4) values!: number[];
170
+ //...
171
+ }
172
+ ```
173
+
174
+ - Fixed-length
175
+ - Safe truncation on assignment
176
+ - Writable by index or full replacement
177
+
178
+ ### Inline Struct
179
+
180
+ ```ts
181
+ {
182
+ //...
183
+ @struct(OtherStruct) child!: OtherStruct;
184
+ //...
185
+ }
186
+ ```
187
+
188
+ Equivalent to:
189
+
190
+ ```c
191
+ struct Parent {
192
+ struct OtherStruct child;
193
+ };
194
+ ```
195
+
196
+ ### Struct Pointer
197
+
198
+ ```ts
199
+ {
200
+ //...
201
+ @ref(OtherStruct) ref!: OtherStruct;
202
+ //...
203
+ }
204
+ ```
205
+
206
+ Equivalent to:
207
+
208
+ ```c
209
+ struct Parent {
210
+ struct OtherStruct* ref;
211
+ };
212
+ ```
213
+
214
+ Multiple references to the same pointer share memory.
215
+
216
+ ### Raw Pointer Access
217
+
218
+ When using `@ref(...)`, you can access the underlying pointer via `$yourKey`.
219
+
220
+ The decorator `@refPointer` is a pass-through helper. It improves type checking and validates that you are referencing an existing `@ref` field.
221
+
222
+ ```ts
223
+ {
224
+ //...
225
+ @ref(OtherStruct) ref!: OtherStruct;
226
+ @refPointer $ref!: Pointer;
227
+ //...
228
+ }
229
+ ```
230
+
231
+ ## Memory Layout
232
+
233
+ - Field order defines memory order.
234
+ - Layout matches C struct layout expectations.
235
+ - Nested structs are inlined.
236
+ - `@ref()` fields store pointers.
237
+ - Arrays and fixed strings reserve fixed space.
238
+ - Offsets are computed once at class definition time.
239
+ - Packed structs are not supported.
240
+
241
+ ## Performance
242
+
243
+ This library is a convenience abstraction.
244
+
245
+ - Field access uses getters/setters.
246
+ - There is function call overhead.
247
+ - It is not zero-cost.
248
+ - It is not faster than manual pointer math.
249
+
250
+ The overhead is usually negligible compared to:
251
+
252
+ - FFI boundary crossings
253
+ - Native library calls
254
+ - IO operations
255
+
256
+ If you are writing extremely tight loops where every nanosecond matters, raw buffer access may be more appropriate.
257
+
258
+ ## What This Library Is
259
+
260
+ - A structured way to describe C memory layouts
261
+ - A safer alternative to manual offset math
262
+ - A developer-friendly wrapper around `bun:ffi`
263
+ - A stopgap solution until Bun provides official struct support
264
+
265
+ ## License
266
+
267
+ MIT
package/index.ts ADDED
@@ -0,0 +1,376 @@
1
+ import { CString, type Pointer, read, ptr as pt, toBuffer } from 'bun:ffi';
2
+ import { endianness } from 'os';
3
+
4
+
5
+ function alignUp(n: number, alignment: number) {
6
+ return (n + alignment - 1) & ~(alignment - 1);
7
+ }
8
+
9
+ const p = Symbol.for('p');
10
+ const o = Symbol.for('o');
11
+ const e = endianness();
12
+
13
+ type Writes = keyof Buffer;
14
+
15
+ const read2w = {
16
+ "f32": `writeFloat${e}` as const,
17
+ 'f64': `writeDouble${e}` as const,
18
+ 'i16': `writeInt16${e}` as const,
19
+ 'u16': `writeUInt16${e}` as const,
20
+ 'i32': `writeInt32${e}` as const,
21
+ 'u32': `writeUInt32${e}` as const,
22
+ 'i64': `writeBigInt64${e}` as const,
23
+ 'u64': `writeBigUInt64${e}` as const,
24
+
25
+ 'intptr': `writeUInt32${e}` as const, // see if we can find the size to use
26
+ 'ptr': `writeBigUInt64${e}` as const, // see if we can find the size to use
27
+
28
+ 'i8': 'writeInt8' as const,
29
+ 'u8': 'writeUInt8' as const,
30
+ } satisfies Record<keyof typeof read, Writes>;
31
+
32
+ type Layout = {
33
+ cursor: number
34
+ alignment: number
35
+ }
36
+ const layoutMap = new WeakMap<Function, Layout>();
37
+
38
+ export abstract class CStruct {
39
+ private [p]: Pointer;
40
+ private [o]: number;
41
+
42
+
43
+ private static getLayout(this: typeof CStruct): Layout {
44
+ let layout = layoutMap.get(this);
45
+ if (!layout) {
46
+ layout = { cursor: 0, alignment: 1 };
47
+ layoutMap.set(this, layout);
48
+ }
49
+ return layout;
50
+ }
51
+
52
+ static get size() {
53
+ const { cursor, alignment } = this.getLayout();
54
+ return alignUp(cursor, alignment);
55
+ }
56
+
57
+ static get alignement() {
58
+ return this.getLayout().alignment;
59
+ }
60
+
61
+ static reader(ptr: Pointer, offset: number) {
62
+ //@ts-ignore
63
+ return new this(ptr, offset);
64
+ }
65
+
66
+ static alloc(): Pointer {
67
+ const buff = Buffer.alloc(this.size);
68
+ return pt(buff);
69
+ }
70
+
71
+
72
+ constructor(ptr: Pointer, offset = 0) {
73
+ this[p] = ptr;
74
+ this[o] = offset;
75
+ Object.defineProperties(this, Object.getOwnPropertyDescriptors(Object.getPrototypeOf(this)))
76
+ }
77
+ }
78
+
79
+ export function chars(length: number) {
80
+ const alignment = 1;
81
+ const size = length;
82
+ const reader = (p: Pointer, offset: number) => {
83
+ return new CString(p, offset).toString();
84
+ }
85
+ const writer = (v: string, p: Pointer, offset: number) => {
86
+ toBuffer(p, offset, size).write(v.padEnd(length, '\0'));
87
+ }
88
+
89
+ const deco = function (target: CStruct, key: string) {
90
+ const ctr = target.constructor as typeof CStruct;
91
+ const layout = ctr["getLayout"]();
92
+ const offset = alignUp(layout.cursor, alignment);
93
+
94
+ layout.cursor = offset + size;
95
+ layout.alignment = Math.max(layout.alignment, alignment);
96
+
97
+ Object.defineProperty(target, key, {
98
+ get() {
99
+ return reader(this[p], offset + this[o]);
100
+ },
101
+ set(v: string) {
102
+ writer(v, this[p], offset + this[o]);
103
+ },
104
+ enumerable: true,
105
+ });
106
+ };
107
+ (deco as any).reader = reader;
108
+ (deco as any).writer = writer;
109
+ (deco as any).size = length;
110
+ (deco as any).alignment = 1;
111
+
112
+ return deco as TypedDecorator<string>;
113
+ }
114
+
115
+ export function struct<T extends typeof CStruct>(cls: T) {
116
+ const reader = (p: Pointer, offset: number) => {
117
+ return cls.reader(p, offset);
118
+ }
119
+ const deco = function (target: CStruct, key: string) {
120
+ const ctr = target.constructor as typeof CStruct;
121
+ const layout = ctr['getLayout']();
122
+
123
+ const alignment = cls['getLayout']().alignment;
124
+ const offset = alignUp(layout.cursor, alignment);
125
+
126
+ layout.cursor = offset + cls.size;
127
+ layout.alignment = Math.max(layout.alignment, alignment);
128
+
129
+ Object.defineProperty(target, key, {
130
+ get() {
131
+ const ptr: Pointer = this[p];
132
+ return reader(ptr, offset + this[o]);
133
+ },
134
+ set(v) {
135
+ throw new Error("Can only write to fields");
136
+ },
137
+ enumerable: true,
138
+ });
139
+ };
140
+
141
+ (deco as any).size = cls.size;
142
+ (deco as any).alignment = cls['getLayout']().alignment;
143
+ (deco as any).reader = reader;
144
+ return deco as unknown as TypedDecorator<InstanceType<T>>;
145
+ }
146
+
147
+ export function array<T extends (typeof CStruct | TypedDecorator<any>)>(itemType: T, count: number) {
148
+
149
+ const size = (itemType as any).size * count;
150
+ const alignment = (itemType as any).alignment;
151
+
152
+ let proxyMemo: any[];
153
+
154
+ const reader = (ptr: Pointer, offset: number) => {
155
+ if (proxyMemo) return proxyMemo;
156
+ proxyMemo = new Proxy(Array(count), {
157
+ get(target, p, receiver) {
158
+ if (typeof p === "symbol" || Number.isNaN(Number(p))) {
159
+ return target[p as any]
160
+ };
161
+ const i = Number(p);
162
+ if (i > count - 1) {
163
+ return undefined;
164
+ }
165
+ return (itemType as any).reader(ptr, offset + i * (itemType as any).size)
166
+ },
167
+ set(target, p, newValue, receiver) {
168
+ const i = Number(p);
169
+ if (i > count - 1) {
170
+ return true;
171
+ }
172
+ (itemType as any).writer(newValue, ptr, offset + i * (itemType as any).size)
173
+ return true
174
+ },
175
+ });
176
+ // const arr = [];
177
+ // for (let i = 0; i < count; i++) {
178
+ // arr.push((itemType as any).reader(ptr, offset + i * (itemType as any).size));
179
+ // }
180
+ return proxyMemo;
181
+ };
182
+
183
+ const writer = (v: any[], ptr: Pointer, offset: number) => {
184
+ for (let i = 0; i < Math.min(v.length, count); i++) {
185
+ (itemType as any).writer(v[i], ptr, offset + i * (itemType as any).size)
186
+ }
187
+ }
188
+
189
+ const deco = function (target: CStruct, key: string) {
190
+ const ctr = target.constructor as typeof CStruct;
191
+ const layout = ctr['getLayout']();
192
+
193
+ const offset = alignUp(layout.cursor, alignment);
194
+ layout.cursor = offset + size;
195
+ layout.alignment = Math.max(layout.alignment, alignment);
196
+
197
+ Object.defineProperty(target, key, {
198
+ get() {
199
+ return reader(this[p], offset + this[o]);
200
+ },
201
+ set(v: any[]) {
202
+ writer(v, this[p], offset + this[o])
203
+ },
204
+ enumerable: true,
205
+ });
206
+ };
207
+
208
+ (deco as any).size = size;
209
+ (deco as any).alignment = alignment;
210
+ (deco as any).reader = reader;
211
+ (deco as any).writer = writer;
212
+
213
+ return deco as T extends typeof CStruct ? TypedDecorator<InstanceType<T>[]> :
214
+ TypedDecorator<Extract<T>[]>;
215
+ }
216
+
217
+
218
+ export function ref<T extends (typeof CStruct | TypedDecorator<any>)>(type: T) {
219
+ const SIZE = 8;
220
+ const ALIGNMENT = 8;
221
+ const reader = (ptr: Pointer, offset: number) => {
222
+ const addr = read.ptr(ptr, offset);
223
+ if (!addr) return null;
224
+ return (type as any).reader(addr, 0)
225
+ };
226
+
227
+ const writer = (v: any, ptr: Pointer, offset: number) => {
228
+ const addr = read.ptr(ptr, offset);
229
+ (type as any).writer(v, addr, 0);
230
+ };
231
+
232
+ const deco = function (target: CStruct, key: string) {
233
+ const ctr = target.constructor as typeof CStruct;
234
+ const layout = ctr['getLayout']();
235
+
236
+ const offset = alignUp(layout.cursor, ALIGNMENT);
237
+ layout.cursor = offset + SIZE;
238
+ layout.alignment = Math.max(layout.alignment, ALIGNMENT);
239
+ Object.defineProperty(target, key, {
240
+ get() {
241
+ return reader(this[p], offset + this[o]);
242
+ },
243
+ set(v) {
244
+ writer(v, this[p], offset + this[o])
245
+ },
246
+ enumerable: true,
247
+ });
248
+
249
+ Object.defineProperty(target, `$${key}`, {
250
+ get() {
251
+ return read.ptr(this[p], offset + this[o]);
252
+ },
253
+ set(v) {
254
+ toBuffer(this[p], offset + this[o], SIZE)[read2w['ptr']](BigInt(v));
255
+ },
256
+ enumerable: false,
257
+ })
258
+ };
259
+ (deco as any).size = SIZE;
260
+ (deco as any).alignment = ALIGNMENT;
261
+ (deco as any).reader = reader;
262
+ (deco as any).writer = writer;
263
+
264
+
265
+ return deco as T extends typeof CStruct ?
266
+ TypedDecorator<InstanceType<T>> : T;
267
+ }
268
+
269
+ function stringDecorator() {
270
+ const SIZE = 8;
271
+ const ALIGNMENT = 8;
272
+ const reader = (ptr: Pointer, offset: number) => {
273
+ const addr = read.ptr(ptr, offset) as Pointer;
274
+ if (!addr) return null;
275
+ return new CString(addr, 0).toString()
276
+ };
277
+
278
+ const writer = (v: string, ptr: Pointer, offset: number) => {
279
+ const toWrite = Buffer.from(`${v}\0`);
280
+ const val = pt(toWrite)
281
+ toBuffer(ptr, offset, SIZE)[read2w['ptr']](BigInt(val));
282
+ };
283
+
284
+ const deco = function (target: CStruct, key: string) {
285
+ const ctr = target.constructor as typeof CStruct;
286
+ const layout = ctr['getLayout']();
287
+
288
+ const offset = alignUp(layout.cursor, ALIGNMENT);
289
+ layout.cursor = offset + SIZE;
290
+ layout.alignment = Math.max(layout.alignment, ALIGNMENT);
291
+
292
+ Object.defineProperty(target, key, {
293
+ get() {
294
+ return reader(this[p], offset + this[o]);
295
+ },
296
+ set(v) {
297
+ writer(v, this[p], offset + this[o])
298
+ },
299
+ enumerable: true,
300
+ });
301
+ };
302
+ (deco as any).size = SIZE;
303
+ (deco as any).alignment = ALIGNMENT;
304
+ (deco as any).reader = reader;
305
+ (deco as any).writer = writer;
306
+
307
+ return deco as TypedDecorator<string>
308
+ }
309
+
310
+
311
+
312
+ function primitiveDecorator(
313
+ size: number,
314
+ readerKey: keyof typeof read,
315
+ alignment = size
316
+ ) {
317
+ const r = read[readerKey];
318
+ const reader = (p: Pointer, offset: number) => {
319
+ return r(p, offset)
320
+ }
321
+ const writer = (v: any, p: Pointer, offset: number) => {
322
+ toBuffer(p, offset, size)[read2w[readerKey]](v as never);
323
+ }
324
+
325
+ const deco = function (target: CStruct, key: string) {
326
+ const ctr = target.constructor as typeof CStruct;
327
+ const layout = ctr['getLayout']();
328
+
329
+ const offset = alignUp(layout.cursor, alignment);
330
+ layout.cursor = offset + size;
331
+ layout.alignment = Math.max(layout.alignment, alignment);
332
+
333
+ Object.defineProperty(target, key, {
334
+ get() {
335
+ return reader(this[p], offset + this[o]);
336
+ },
337
+ set(v) {
338
+ writer(v, this[p], offset + this[o]);
339
+ },
340
+ enumerable: true,
341
+ });
342
+ };
343
+
344
+ (deco as any).size = size;
345
+ (deco as any).alignment = alignment;
346
+ (deco as any).reader = reader;
347
+ (deco as any).writer = writer;
348
+
349
+ return deco;
350
+ }
351
+
352
+ export const i8 = primitiveDecorator(1, 'i8', 1) as TypedDecorator<number>;
353
+ export const u8 = primitiveDecorator(1, 'u8', 1) as TypedDecorator<number>;
354
+ export const i16 = primitiveDecorator(2, 'i16', 2) as TypedDecorator<number>;
355
+ export const u16 = primitiveDecorator(2, 'u16', 2) as TypedDecorator<number>;
356
+
357
+ export const i32 = primitiveDecorator(4, 'i32', 4) as TypedDecorator<number>;
358
+ export const u32 = primitiveDecorator(4, 'u32', 4) as TypedDecorator<number>;
359
+ export const f32 = primitiveDecorator(4, 'f32', 4) as TypedDecorator<number>;
360
+
361
+ export const i64 = primitiveDecorator(8, 'i64', 8) as TypedDecorator<number>;
362
+ export const u64 = primitiveDecorator(8, 'u64', 8) as TypedDecorator<number>;
363
+ export const f64 = primitiveDecorator(8, 'f64', 8) as TypedDecorator<number>;
364
+
365
+ export const ptr = primitiveDecorator(8, 'ptr', 8) as TypedDecorator<number>;
366
+
367
+ export const string = stringDecorator();
368
+
369
+
370
+ export const refPointer = ((a: CStruct, b: string) => { }) as <T extends CStruct, K extends keyof T>(target: T, key: K, ...this_pointer_doesnt_exist_in_this_struct: CheckRefPointer<T, K>) => void;
371
+
372
+
373
+ type CheckRefPointer<T, K extends keyof T> = K extends `$${infer U}` ? U extends keyof T ? [] : [U] : [K];
374
+ type Checks<T, K extends keyof T, Expected> = T[K] extends Expected ? [] : [Expected]
375
+ type TypedDecorator<Expected> = <T extends CStruct, K extends keyof T>(target: T, key: K, ...property_and_decorator_type_mismatch: Checks<T, K, Expected>) => void;
376
+ type Extract<T> = T extends TypedDecorator<infer U> ? U : never
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@obelusfi/bun-cstruct",
3
+ "module": "index.ts",
4
+ "type": "module",
5
+ "version": "0.0.1",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/ObelusFi/bun-cstruct.git"
9
+ },
10
+ "files": [
11
+ "index.ts"
12
+ ],
13
+ "description": "A lightweight convenience layer for working with C structs in bun:ffi using TypeScript decorators.",
14
+ "devDependencies": {
15
+ "@types/bun": "latest"
16
+ },
17
+ "peerDependencies": {
18
+ "typescript": "^5"
19
+ },
20
+ "license": "MIT",
21
+ "keywords": [
22
+ "bun",
23
+ "ffi",
24
+ "c-struct",
25
+ "typescript",
26
+ "decorators",
27
+ "native",
28
+ "pointer",
29
+ "struct",
30
+ "memory",
31
+ "interop"
32
+ ]
33
+ }