@retrovm/nobj 0.1.0

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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +135 -0
  3. package/nobj.ts +523 -0
  4. package/package.json +22 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Juan Carlos González Amestoy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # @retrovm/nobj
2
+
3
+ Generate native linkable object files — **COFF**, **Mach-O** and **ELF64** — containing exported read-only data symbols, directly from TypeScript/Bun.
4
+
5
+ No native dependencies. No spawn. No temp files. Just a `Buffer` out.
6
+
7
+ ---
8
+
9
+ ## The problem
10
+
11
+ Embedding binary assets in a C/C++ project is usually done as a static array:
12
+
13
+ ```c
14
+ // generated by xxd or similar
15
+ static const unsigned char font_data[] = {
16
+ 0x00, 0x01, 0x02, 0x03, /* ... 512 KB of this ... */
17
+ };
18
+ ```
19
+
20
+ This works, but it doesn't scale. Clang has to parse, lex and codegen every single byte as a numeric literal. A 1 MB asset becomes a translation unit with a million tokens — compilation slows to a crawl and RAM usage spikes. With several large assets the build becomes the bottleneck.
21
+
22
+ The right fix is to put the data in a proper object file and let the linker handle placement. This library generates that object file in pure TypeScript, with no external tools required.
23
+
24
+ ---
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ bun add @retrovm/nobj
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Usage
35
+
36
+ ```ts
37
+ import { encodeSymbols } from '@retrovm/nobj'
38
+
39
+ const buf = encodeSymbols([
40
+ { name: 'font_data', obj: Buffer.from(await Bun.file('font.bin').bytes()) },
41
+ { name: 'shader_src', obj: '#version 330\nvoid main() {}' },
42
+ { name: 'sample_rate', obj: 44100.0 },
43
+ ])
44
+
45
+ await Bun.write('assets.o', buf)
46
+ ```
47
+
48
+ Link normally:
49
+
50
+ ```bash
51
+ # macOS
52
+ clang main.c assets.o -o app
53
+
54
+ # Linux
55
+ clang main.c assets.o -o app
56
+
57
+ # Windows
58
+ clang-cl main.c assets.obj -o app.exe
59
+ ```
60
+
61
+ Reference from C with no runtime overhead:
62
+
63
+ ```c
64
+ extern const unsigned char font_data[];
65
+ extern const char shader_src[];
66
+ extern const double sample_rate;
67
+ ```
68
+
69
+ ---
70
+
71
+ ## API
72
+
73
+ ### `encodeObject(name, obj, arch?, platform?)`
74
+
75
+ Encodes a single symbol.
76
+
77
+ ```ts
78
+ const buf = encodeObject('palette', paletteBuffer)
79
+ ```
80
+
81
+ ### `encodeSymbols(symbols[], arch?, platform?)`
82
+
83
+ Encodes multiple symbols into one object file. Preferred for production — one file, one link step.
84
+
85
+ ```ts
86
+ const buf = encodeSymbols([
87
+ { name: 'icon_png', obj: iconBuffer },
88
+ { name: 'main_glsl', obj: shaderSource },
89
+ ])
90
+ ```
91
+
92
+ ### Symbol values
93
+
94
+ | TypeScript type | C declaration | Notes |
95
+ |-----------------|--------------------------|----------------------------------------|
96
+ | `Buffer` | `const uint8_t[]` | Raw bytes, no transformation |
97
+ | `string` | `const char[]` | ASCII, null-terminated (except macOS) |
98
+ | `number` | `const double` | IEEE 754, 8 bytes |
99
+
100
+ ### Cross-compilation
101
+
102
+ By default both platform and architecture are inferred from the running process. Override to cross-compile:
103
+
104
+ ```ts
105
+ encodeSymbols(symbols, 'arm64', 'linux') // Linux/arm64
106
+ encodeSymbols(symbols, 'x64', 'win32') // Windows/x64
107
+ encodeSymbols(symbols, 'arm64', 'darwin') // macOS/arm64
108
+ ```
109
+
110
+ | `TargetPlatform` | Format | Section |
111
+ |------------------|--------|-----------|
112
+ | `'win32'` | COFF | `.rdata` |
113
+ | `'darwin'` | Mach-O | `__const` |
114
+ | `'linux'` | ELF64 | `.rodata` |
115
+
116
+ | `TargetArch` | COFF machine | Mach-O cputype |
117
+ |--------------|--------------|----------------|
118
+ | `'x64'` | `0x8664` | `0x1000007` |
119
+ | `'arm64'` | `0xAA64` | `0x100000C` |
120
+
121
+ ---
122
+
123
+ ## Format notes
124
+
125
+ The generated files are minimal but correct — accepted by `clang`, `lld` and `ld` without warnings, and readable by `llvm-readobj`, `dumpbin` and `objdump`.
126
+
127
+ - **COFF** — includes a `.drectve` section with `/EXPORT` directives and `@feat.00` to satisfy MSVC/clang-cl safe-SEH requirements.
128
+ - **Mach-O** — emits `LC_BUILD_VERSION` targeting macOS 10.9 so modern `ld` accepts the file without deprecation warnings. Symbol values are section-relative offsets, as required for relocatable objects.
129
+ - **ELF64** — uses a single `.strtab` as both `sh_strtab` and symbol string table. Includes an empty `.note.GNU-stack` to mark the stack as non-executable.
130
+
131
+ ---
132
+
133
+ ## License
134
+
135
+ 2026 - MIT © Juan Carlos González Amestoy
package/nobj.ts ADDED
@@ -0,0 +1,523 @@
1
+ /*Copyright (c) 2026 Juan Carlos González Amestoy
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.*/
20
+
21
+ export type SymbolValue = Buffer | string | number;
22
+
23
+ export interface ObjSymbol {
24
+ name: string;
25
+ obj: SymbolValue;
26
+ }
27
+
28
+ export type TargetPlatform = 'win32' | 'darwin' | 'linux';
29
+ export type TargetArch = 'x64' | 'arm64';
30
+
31
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
32
+
33
+ /** Align v up to the next multiple of a (a must be a power of two). */
34
+ const al = (v: number, a: number): number => (v + a - 1) & -a;
35
+
36
+ /**
37
+ * Write a 64-bit LE uint using two 32-bit writes.
38
+ * Safe for values ≤ 2^53 (all realistic file offsets and sizes).
39
+ */
40
+ function writeU64(buf: Buffer, off: number, val: number): void {
41
+ buf.writeUInt32LE(val >>> 0, off);
42
+ buf.writeUInt32LE(Math.floor(val / 0x100000000), off + 4);
43
+ }
44
+
45
+ /** Zero a char[len] field and write an ASCII string (no overflow). */
46
+ function setStr(buf: Buffer, off: number, str: string, len: number): void {
47
+ buf.fill(0, off, off + len);
48
+ buf.write(str.substring(0, len), off, 'ascii');
49
+ }
50
+
51
+ function hostArch(): TargetArch {
52
+ if (process.arch === 'x64') return 'x64';
53
+ if (process.arch === 'arm64') return 'arm64';
54
+ throw new Error(`Unsupported host architecture: ${process.arch}`);
55
+ }
56
+
57
+ // ═══════════════════════════════════════════════════════════════════════════════
58
+ // Windows — COFF
59
+ // ═══════════════════════════════════════════════════════════════════════════════
60
+ //
61
+ // Packed struct layout (all little-endian, "Coff" = 190 bytes):
62
+ //
63
+ // CoffHeader 20 @ 0
64
+ // CoffSection[0] 40 @ 20 (.drectve)
65
+ // CoffSection[1] 40 @ 60 (.rdata)
66
+ // CoffSymbol 18 @ 100 (.rdata section symbol)
67
+ // CoffAuxSymbol 18 @ 118
68
+ // CoffSymbol 18 @ 136 (.drectve section symbol)
69
+ // CoffAuxSymbol 18 @ 154
70
+ // CoffSymbol 18 @ 172 (@feat.00)
71
+ //
72
+ // File layout: [hd:190][symd:N×18][tsd:strTable][tsed:directives+pad][dsd:data]
73
+ //
74
+ // Symbol table starts at file offset 100 (= ffi.offsetof('Coff','rdata')).
75
+ // The 5 built-in entries live inside hd[100..189]; the N per-export entries
76
+ // follow immediately in symd, making the table contiguous.
77
+
78
+ const COFF_TOTAL_SZ = 190;
79
+ const COFF_OFFSETOF_RDATA = 100; // ffi.offsetof('Coff', 'rdata')
80
+ const COFF_SYM_SZ = 18;
81
+ const COFF_HDR_SZ = 20;
82
+ const COFF_SECT_SZ = 40;
83
+
84
+ function doObjectWindows(symbols: ObjSymbol[], arch: TargetArch): Buffer {
85
+ let strTabSz = 0, dataSz = 0;
86
+ const directives: string[] = [];
87
+
88
+ for (const { name, obj } of symbols) {
89
+ strTabSz += name.length + 1;
90
+ directives.push(` /EXPORT:${name},DATA`);
91
+ if (Buffer.isBuffer(obj)) dataSz += al(obj.length, 8);
92
+ else if (typeof obj === 'string') dataSz += al(obj.length + 1, 8);
93
+ else if (typeof obj === 'number') dataSz += 8;
94
+ else throw new Error('Invalid symbol type');
95
+ }
96
+
97
+ const tsd = Buffer.alloc(strTabSz + 4); // COFF string table
98
+ const dsd = Buffer.alloc(dataSz); // .rdata data
99
+ const tsed = Buffer.from(directives.join(''), 'ascii'); // .drectve content
100
+ const symd = Buffer.alloc(COFF_SYM_SZ * symbols.length); // per-export symbols
101
+
102
+ tsd.writeUInt32LE(strTabSz + 4, 0); // string table size prefix
103
+
104
+ let strOff = 4, dataOff = 0;
105
+
106
+ for (let i = 0; i < symbols.length; i++) {
107
+ const { name, obj } = symbols[i]!;
108
+ const b = i * COFF_SYM_SZ;
109
+
110
+ // CoffSymbol: long name (zeros=0 means use string table, offset=strOff)
111
+ symd.writeUInt32LE(0, b + 0); // name.zeros
112
+ symd.writeUInt32LE(strOff, b + 4); // name.offset → string table
113
+ symd.writeUInt32LE(dataOff, b + 8); // value = offset in .rdata
114
+ symd.writeInt16LE(2, b + 12); // sectionNumber: .rdata = 2
115
+ symd.writeUInt16LE(0, b + 14); // type
116
+ symd.writeUInt8(0x2, b + 16); // storageClass: IMAGE_SYM_CLASS_EXTERNAL
117
+ symd.writeUInt8(0, b + 17); // numberOfAuxSymbols
118
+
119
+ tsd.write(name, strOff, 'ascii');
120
+ strOff += name.length + 1;
121
+
122
+ if (Buffer.isBuffer(obj)) {
123
+ obj.copy(dsd, dataOff);
124
+ dataOff += al(obj.length, 8);
125
+ } else if (typeof obj === 'string') {
126
+ dsd.write(obj, dataOff, 'ascii');
127
+ dataOff += al(obj.length + 1, 8);
128
+ } else if (typeof obj === 'number') {
129
+ dsd.writeDoubleLE(obj, dataOff);
130
+ dataOff += 8;
131
+ }
132
+ }
133
+
134
+ // ── Coff header struct ──────────────────────────────────────────────────────
135
+
136
+ const hd = Buffer.alloc(COFF_TOTAL_SZ);
137
+ const machine = arch === 'x64' ? 0x8664 : 0xaa64;
138
+ const ts = Math.floor(Date.now() / 1000);
139
+
140
+ // CoffHeader @ 0
141
+ hd.writeUInt16LE(machine, 0);
142
+ hd.writeUInt16LE(2, 2); // numberOfSections
143
+ hd.writeUInt32LE(ts, 4); // timeDateStamp
144
+ hd.writeUInt32LE(COFF_OFFSETOF_RDATA, 8); // pointerToSymbolTable
145
+ hd.writeUInt32LE(symbols.length + 5, 12); // numberOfSymbols (5 built-in + N exports)
146
+ // sizeOfOptionalHeader=0, flags=0 (zeroed)
147
+
148
+ // CoffSection[0]: .drectve @ 20
149
+ const drectvePtr = COFF_TOTAL_SZ + symd.length + tsd.length;
150
+ setStr(hd, COFF_HDR_SZ, '.drectve', 8);
151
+ hd.writeUInt32LE(tsed.length, COFF_HDR_SZ + 16); // sizeOfRawData
152
+ hd.writeUInt32LE(drectvePtr, COFF_HDR_SZ + 20); // pointerToRawData
153
+ hd.writeUInt32LE(0x00100a00, COFF_HDR_SZ + 36); // flags
154
+
155
+ // CoffSection[1]: .rdata @ 60
156
+ const rdataPtr = al(COFF_TOTAL_SZ + symd.length + tsd.length + tsed.length, 8);
157
+ setStr(hd, COFF_HDR_SZ + COFF_SECT_SZ, '.rdata', 8);
158
+ hd.writeUInt32LE(dsd.length, COFF_HDR_SZ + COFF_SECT_SZ + 16);
159
+ hd.writeUInt32LE(rdataPtr, COFF_HDR_SZ + COFF_SECT_SZ + 20);
160
+ hd.writeUInt32LE(0x40300040, COFF_HDR_SZ + COFF_SECT_SZ + 36);
161
+
162
+ // CoffSymbol: .rdata section symbol @ 100
163
+ setStr(hd, 100, '.rdata', 8);
164
+ hd.writeInt16LE(2, 100 + 12); // sectionNumber
165
+ hd.writeUInt8(0x3, 100 + 16); // storageClass: IMAGE_SYM_CLASS_STATIC
166
+ hd.writeUInt8(1, 100 + 17); // numberOfAuxSymbols
167
+
168
+ // CoffAuxSymbol for .rdata @ 118
169
+ hd.writeUInt32LE(dsd.length, 118 + 0); // length
170
+ hd.writeUInt16LE(2, 118 + 12); // number (= section index)
171
+
172
+ // CoffSymbol: .drectve section symbol @ 136
173
+ setStr(hd, 136, '.drectve', 8);
174
+ hd.writeInt16LE(1, 136 + 12);
175
+ hd.writeUInt8(0x3, 136 + 16);
176
+ hd.writeUInt8(1, 136 + 17);
177
+
178
+ // CoffAuxSymbol for .drectve @ 154
179
+ hd.writeUInt32LE(tsed.length, 154 + 0);
180
+ hd.writeUInt16LE(1, 154 + 12);
181
+
182
+ // CoffSymbol: @feat.00 @ 172
183
+ setStr(hd, 172, '@feat.00', 8);
184
+ hd.writeInt16LE(-1, 172 + 12); // IMAGE_SYM_ABSOLUTE
185
+ hd.writeUInt8(0x3, 172 + 16);
186
+
187
+ // Pad tsed to 8-byte boundary so .rdata starts aligned
188
+ const pad = rdataPtr - (COFF_TOTAL_SZ + symd.length + tsd.length + tsed.length);
189
+ const tsedFinal = pad > 0 ? Buffer.concat([tsed, Buffer.alloc(pad)]) : tsed;
190
+
191
+ return Buffer.concat([hd, symd, tsd, tsedFinal, dsd]);
192
+ }
193
+
194
+ // ═══════════════════════════════════════════════════════════════════════════════
195
+ // macOS — Mach-O
196
+ // ═══════════════════════════════════════════════════════════════════════════════
197
+ //
198
+ // mach_obj struct layout (total 0x188 = 392 bytes):
199
+ //
200
+ // mach_header_64 32 @ 0
201
+ // mach_load_segment_cmd 72 @ 32
202
+ // mach_section_64 __text 80 @ 104
203
+ // mach_section_64 __const 80 @ 184
204
+ // mach_minimun_os_command 24 @ 264
205
+ // mach_symtab_command 24 @ 288
206
+ // mach_symtab_info 80 @ 312
207
+ //
208
+ // File layout: [hd:0x188][dt:data][nti:symEntries][nt:stringTable]
209
+ //
210
+ // mach_sym_entry: u32 strx | u8 type | u8 sect | u16 desc | u64 value = 16 bytes
211
+ // String table: '\0' | ('_' + name + '\0') × N — first byte is always null
212
+
213
+ const MACH_HDR_SZ = 0x188; // = 392
214
+
215
+ // Byte offsets within hd
216
+ const MH = 0; // mach_header_64
217
+ const MSEG = 32; // mach_load_segment_command
218
+ const MS1 = 104; // mach_section_64[0] __text (empty)
219
+ const MS2 = 184; // mach_section_64[1] __const (data)
220
+ const MMO = 264; // mach_minimun_os_command
221
+ const MSYM = 288; // mach_symtab_command
222
+ const MDYS = 312; // mach_symtab_info (LC_DYSYMTAB)
223
+
224
+ function doObjectMacOS(symbols: ObjSymbol[], arch: TargetArch): Buffer {
225
+ let dataSz = 0, strTabSz = 1, symTabSz = 0;
226
+
227
+ for (const { name, obj } of symbols) {
228
+ if (Buffer.isBuffer(obj)) dataSz += al(obj.length, 16);
229
+ else if (typeof obj === 'string') dataSz += al(obj.length, 16); // raw bytes, no null
230
+ else if (typeof obj === 'number') dataSz += al(8, 16); // = 16
231
+ else throw new Error('Invalid symbol type');
232
+ strTabSz += name.length + 2; // '_' + name + '\0'
233
+ symTabSz += 16; // sizeof(mach_sym_entry)
234
+ }
235
+ strTabSz = al(strTabSz, 8);
236
+
237
+ const dt = Buffer.alloc(dataSz); // data section
238
+ const nt = Buffer.alloc(strTabSz); // string table (first byte is '\0' by alloc)
239
+ const nti = Buffer.alloc(symTabSz); // symbol entries
240
+
241
+ let dataOff = 0, strOff = 1;
242
+
243
+ for (let k = 0; k < symbols.length; k++) {
244
+ const { name, obj } = symbols[k]!;
245
+ const valueInSection = dataOff;
246
+
247
+ if (Buffer.isBuffer(obj)) {
248
+ obj.copy(dt, dataOff);
249
+ dataOff += al(obj.length, 16);
250
+ } else if (typeof obj === 'string') {
251
+ dt.write(obj, dataOff, 'ascii');
252
+ dataOff += al(obj.length, 16);
253
+ } else if (typeof obj === 'number') {
254
+ dt.writeDoubleLE(obj, dataOff);
255
+ dataOff += al(8, 16);
256
+ }
257
+
258
+ nt.write('_' + name, strOff, 'ascii'); // null terminator from Buffer.alloc
259
+
260
+ // mach_sym_entry @ k*16
261
+ const e = k * 16;
262
+ nti.writeUInt32LE(strOff, e + 0); // strx
263
+ nti.writeUInt8(0xf, e + 4); // type: N_SECT | N_EXT
264
+ nti.writeUInt8(2, e + 5); // sect: 2 = __const
265
+ nti.writeUInt16LE(0, e + 6); // desc
266
+ writeU64(nti, e + 8, valueInSection); // value = offset within section
267
+
268
+ strOff += name.length + 2;
269
+ }
270
+
271
+ const hd = Buffer.alloc(MACH_HDR_SZ);
272
+
273
+ // mach_header_64
274
+ hd.writeUInt32LE(0xfeedfacf, MH + 0); // MH_MAGIC_64
275
+ hd.writeUInt32LE(arch === 'x64' ? 0x1000007 : 0x100000c, MH + 4); // cputype
276
+ hd.writeUInt32LE(arch === 'x64' ? 0x3 : 0x0, MH + 8); // cpusubtype
277
+ hd.writeUInt32LE(0x1, MH + 12); // filetype: MH_OBJECT
278
+ hd.writeUInt32LE(4, MH + 16); // ncmds: 4 load commands
279
+ hd.writeUInt32LE(0x168, MH + 20); // sizeofcmds = 0xE8+0x18+0x18+0x50 = 360
280
+ hd.writeUInt32LE(0x200, MH + 24); // flags: MH_SUBSECTIONS_VIA_SYMBOLS
281
+
282
+ // LC_SEGMENT_64 (cmd=0x19, cmdsize=0xE8 = 72 + 80×2)
283
+ hd.writeUInt32LE(0x19, MSEG + 0);
284
+ hd.writeUInt32LE(0xE8, MSEG + 4);
285
+ // segname[16]: all zeros = unnamed segment (correct for .o files)
286
+ writeU64(hd, MSEG + 24, 0); // vmaddr
287
+ writeU64(hd, MSEG + 32, dataSz); // vmsize
288
+ writeU64(hd, MSEG + 40, MACH_HDR_SZ); // fileoff
289
+ writeU64(hd, MSEG + 48, dataSz); // filesize
290
+ hd.writeUInt32LE(0x7, MSEG + 56); // maxprot: PROT_READ|WRITE|EXEC
291
+ hd.writeUInt32LE(0x7, MSEG + 60); // initprot
292
+ hd.writeUInt32LE(2, MSEG + 64); // nsects
293
+
294
+ // mach_section_64[0]: __text (empty — placeholder for the TEXT segment)
295
+ setStr(hd, MS1 + 0, '__text', 16);
296
+ setStr(hd, MS1 + 16, '__TEXT', 16);
297
+ // addr=0, size=0 (zeroed)
298
+ hd.writeUInt32LE(MACH_HDR_SZ, MS1 + 48); // offset
299
+ // align=0
300
+ hd.writeUInt32LE(0x80000000, MS1 + 64); // flags: S_ATTR_PURE_INSTRUCTIONS
301
+
302
+ // mach_section_64[1]: __const (actual exported data)
303
+ setStr(hd, MS2 + 0, '__const', 16);
304
+ setStr(hd, MS2 + 16, '__TEXT', 16);
305
+ // addr=0 (zeroed)
306
+ writeU64(hd, MS2 + 40, dataSz); // size
307
+ hd.writeUInt32LE(MACH_HDR_SZ, MS2 + 48); // offset
308
+ hd.writeUInt32LE(0x02, MS2 + 52); // align: 2^2 = 4 bytes
309
+
310
+ // LC_BUILD_VERSION (cmd=0x32)
311
+ hd.writeUInt32LE(0x32, MMO + 0);
312
+ hd.writeUInt32LE(0x18, MMO + 4);
313
+ hd.writeUInt32LE(0x1, MMO + 8); // platform: PLATFORM_MACOS
314
+ hd.writeUInt32LE(0xa0900, MMO + 12); // minos: 10.9.0
315
+ hd.writeUInt32LE(0xa0900, MMO + 16); // sdk: 10.9.0
316
+
317
+ // LC_SYMTAB (cmd=0x2)
318
+ hd.writeUInt32LE(0x2, MSYM + 0);
319
+ hd.writeUInt32LE(0x18, MSYM + 4);
320
+ hd.writeUInt32LE(MACH_HDR_SZ + dataSz, MSYM + 8); // symoff
321
+ hd.writeUInt32LE(symbols.length, MSYM + 12); // nsyms
322
+ hd.writeUInt32LE(MACH_HDR_SZ + dataSz + symTabSz, MSYM + 16); // stroff
323
+ hd.writeUInt32LE(strTabSz, MSYM + 20); // strsize
324
+
325
+ // LC_DYSYMTAB (cmd=0xb)
326
+ hd.writeUInt32LE(0xb, MDYS + 0);
327
+ hd.writeUInt32LE(0x50, MDYS + 4);
328
+ // localoff=0, nlocals=0 (zeroed)
329
+ hd.writeUInt32LE(symbols.length, MDYS + 20); // nextdef
330
+ hd.writeUInt32LE(symbols.length, MDYS + 24); // undefoff
331
+
332
+ return Buffer.concat([hd, dt, nti, nt]);
333
+ }
334
+
335
+ // ═══════════════════════════════════════════════════════════════════════════════
336
+ // Linux — ELF64
337
+ // ═══════════════════════════════════════════════════════════════════════════════
338
+ //
339
+ // ELF64 struct layout (total 448 bytes):
340
+ //
341
+ // ELF64Header 64 @ 0
342
+ // ELF64Section×6 64 @ 64…447
343
+ // [0] null
344
+ // [1] .symtab
345
+ // [2] .strtab (combined: section names followed by symbol names)
346
+ // [3] .rodata
347
+ // [4] .note.GNU-stack
348
+ // [5] (unused)
349
+ //
350
+ // ELF64Symbol: u32 name | u8 info | u8 other | u16 sectidx | u64 value | u64 size = 24 bytes
351
+ //
352
+ // File layout: [hd:448][symd:symTable][ts:strTable][d:data]
353
+ //
354
+ // .strtab (section 2) is also used as .shstrtab (shstridx=2):
355
+ // it starts with section names (\0.symtab\0.strtab\0.rodata\0.note.GNU-stack\0)
356
+ // immediately followed by the exported symbol names. Both the linker and the
357
+ // section-header string lookup use offsets into this single buffer.
358
+
359
+ const ELF_STRUCT_SZ = 448;
360
+ const ELF_HDR_SZ = 64;
361
+ const ELF_SECT_SZ = 64;
362
+ const ELF_SYM_SZ = 24;
363
+
364
+ // ELF64Section field offsets within one section entry
365
+ const ES = { NAME:0, TYPE:4, FLAGS:8, ADDR:16, OFS:24, SIZE:32, LINK:40, INFO:44, ALIGN:48, ENTSZ:56 };
366
+ // ELF64Symbol field offsets
367
+ const EY = { NAME:0, INFO:4, OTHER:5, SECTIDX:6, VALUE:8, SIZE:16 };
368
+
369
+ const sectOfs = (idx: number): number => ELF_HDR_SZ + idx * ELF_SECT_SZ;
370
+
371
+ function doObjectLinux(symbols: ObjSymbol[], arch: TargetArch): Buffer {
372
+ const SH_NAMES = '\0.symtab\0.strtab\0.rodata\0.note.GNU-stack\0';
373
+ // idx: 0 1 9 17 25
374
+ const I_SYMTAB = 1, I_STRTAB = 9, I_RODATA = 17, I_NOTE = 25;
375
+
376
+ let dataSz = 0, strTabSz = SH_NAMES.length;
377
+
378
+ for (const { name, obj } of symbols) {
379
+ strTabSz += name.length + 1;
380
+ if (Buffer.isBuffer(obj)) dataSz += al(obj.length, 8);
381
+ else if (typeof obj === 'string') dataSz += al(obj.length + 1, 8);
382
+ else if (typeof obj === 'number') dataSz += 8;
383
+ else throw new Error('Invalid symbol type');
384
+ }
385
+
386
+ // Symbol table: one null entry (index 0) + one per export
387
+ const symd = Buffer.alloc(al(ELF_SYM_SZ * (symbols.length + 1), 16));
388
+ const ts = Buffer.alloc(al(strTabSz, 16));
389
+ const d = Buffer.alloc(al(dataSz, 16));
390
+
391
+ ts.write(SH_NAMES, 0, 'ascii');
392
+
393
+ let strOff = SH_NAMES.length, dataOff = 0;
394
+
395
+ for (let k = 0; k < symbols.length; k++) {
396
+ const { name, obj } = symbols[k]!;
397
+ const sb = (k + 1) * ELF_SYM_SZ; // skip null symbol at index 0
398
+
399
+ symd.writeUInt32LE(strOff, sb + EY.NAME);
400
+ symd.writeUInt8(0x11, sb + EY.INFO); // STB_GLOBAL|STT_OBJECT = (1<<4)|1
401
+ symd.writeUInt8(0, sb + EY.OTHER);
402
+ symd.writeUInt16LE(3, sb + EY.SECTIDX); // .rodata = section 3
403
+ writeU64(symd, sb + EY.VALUE, dataOff);
404
+
405
+ ts.write(name, strOff, 'ascii');
406
+ strOff += name.length + 1;
407
+
408
+ if (Buffer.isBuffer(obj)) {
409
+ obj.copy(d, dataOff);
410
+ writeU64(symd, sb + EY.SIZE, obj.length);
411
+ dataOff += al(obj.length, 8);
412
+ } else if (typeof obj === 'string') {
413
+ d.write(obj, dataOff, 'ascii');
414
+ writeU64(symd, sb + EY.SIZE, obj.length + 1);
415
+ dataOff += al(obj.length + 1, 8);
416
+ } else if (typeof obj === 'number') {
417
+ d.writeDoubleLE(obj, dataOff);
418
+ writeU64(symd, sb + EY.SIZE, 8);
419
+ dataOff += 8;
420
+ }
421
+ }
422
+
423
+ // ── ELF header + section headers ───────────────────────────────────────────
424
+
425
+ const hd = Buffer.alloc(ELF_STRUCT_SZ);
426
+ const mach = arch === 'x64' ? 0x3e : 0xb7;
427
+
428
+ // ELF64Header @ 0
429
+ hd.write('\x7fELF', 0, 'ascii');
430
+ hd.writeUInt8(2, 4); // EI_CLASS: ELFCLASS64
431
+ hd.writeUInt8(1, 5); // EI_DATA: ELFDATA2LSB
432
+ hd.writeUInt8(1, 6); // EI_VERSION: EV_CURRENT
433
+ // osabi=0, abiversion=0, epad=0
434
+ hd.writeUInt16LE(1, 16); // e_type: ET_REL
435
+ hd.writeUInt16LE(mach, 18); // e_machine
436
+ hd.writeUInt32LE(1, 20); // e_version: EV_CURRENT
437
+ // e_entry=0, e_phoff=0 (zeroed)
438
+ writeU64(hd, 40, ELF_HDR_SZ); // e_shoff: section headers right after hdr
439
+ // e_flags=0 (zeroed)
440
+ hd.writeUInt16LE(ELF_HDR_SZ, 52); // e_ehsize
441
+ // e_phentsize=0, e_phnum=0 (zeroed)
442
+ hd.writeUInt16LE(ELF_SECT_SZ, 58); // e_shentsize
443
+ hd.writeUInt16LE(6, 60); // e_shnum
444
+ hd.writeUInt16LE(2, 62); // e_shstrndx: .strtab = section 2
445
+
446
+ const symtabOfs = ELF_STRUCT_SZ;
447
+ const strtabOfs = ELF_STRUCT_SZ + symd.length;
448
+ const rodataOfs = ELF_STRUCT_SZ + symd.length + ts.length;
449
+ const noteOfs = ELF_STRUCT_SZ + symd.length + ts.length + d.length;
450
+
451
+ // Section[0]: null (all zeros)
452
+
453
+ // Section[1]: .symtab
454
+ const s1 = sectOfs(1);
455
+ hd.writeUInt32LE(I_SYMTAB, s1 + ES.NAME);
456
+ hd.writeUInt32LE(2, s1 + ES.TYPE); // SHT_SYMTAB
457
+ writeU64(hd, s1 + ES.OFS, symtabOfs);
458
+ writeU64(hd, s1 + ES.SIZE, symd.length);
459
+ hd.writeUInt32LE(2, s1 + ES.LINK); // associated .strtab = section 2
460
+ hd.writeUInt32LE(1, s1 + ES.INFO); // first global symbol index
461
+ writeU64(hd, s1 + ES.ALIGN, 8);
462
+ writeU64(hd, s1 + ES.ENTSZ, ELF_SYM_SZ);
463
+
464
+ // Section[2]: .strtab
465
+ const s2 = sectOfs(2);
466
+ hd.writeUInt32LE(I_STRTAB, s2 + ES.NAME);
467
+ hd.writeUInt32LE(3, s2 + ES.TYPE); // SHT_STRTAB
468
+ writeU64(hd, s2 + ES.OFS, strtabOfs);
469
+ writeU64(hd, s2 + ES.SIZE, ts.length);
470
+
471
+ // Section[3]: .rodata
472
+ const s3 = sectOfs(3);
473
+ hd.writeUInt32LE(I_RODATA, s3 + ES.NAME);
474
+ hd.writeUInt32LE(1, s3 + ES.TYPE); // SHT_PROGBITS
475
+ writeU64(hd, s3 + ES.FLAGS, 2); // SHF_ALLOC
476
+ writeU64(hd, s3 + ES.OFS, rodataOfs);
477
+ writeU64(hd, s3 + ES.SIZE, d.length);
478
+
479
+ // Section[4]: .note.GNU-stack (empty; signals non-executable stack to linker)
480
+ const s4 = sectOfs(4);
481
+ hd.writeUInt32LE(I_NOTE, s4 + ES.NAME);
482
+ hd.writeUInt32LE(1, s4 + ES.TYPE); // SHT_PROGBITS
483
+ writeU64(hd, s4 + ES.OFS, noteOfs);
484
+ // size=0, flags=0 (zeroed)
485
+
486
+ return Buffer.concat([hd, symd, ts, d]);
487
+ }
488
+
489
+ // ═══════════════════════════════════════════════════════════════════════════════
490
+ // Public API (mirrors object:encodeObject / object:encodeSymbols in Lua)
491
+ // ═══════════════════════════════════════════════════════════════════════════════
492
+
493
+ function dispatch(
494
+ symbols: ObjSymbol[],
495
+ arch: TargetArch = hostArch(),
496
+ platform: TargetPlatform = process.platform as TargetPlatform,
497
+ ): Buffer {
498
+ switch (platform) {
499
+ case 'win32': return doObjectWindows(symbols, arch);
500
+ case 'darwin': return doObjectMacOS(symbols, arch);
501
+ case 'linux': return doObjectLinux(symbols, arch);
502
+ default: throw new Error(`Unsupported platform: ${platform}`);
503
+ }
504
+ }
505
+
506
+ /** Encode a single named symbol into a native linkable object file. */
507
+ export function encodeObject(
508
+ name: string,
509
+ obj: SymbolValue,
510
+ arch?: TargetArch,
511
+ platform?: TargetPlatform,
512
+ ): Buffer {
513
+ return dispatch([{ name, obj }], arch, platform);
514
+ }
515
+
516
+ /** Encode multiple named symbols into a native linkable object file. */
517
+ export function encodeSymbols(
518
+ symbols: ObjSymbol[],
519
+ arch?: TargetArch,
520
+ platform?: TargetPlatform,
521
+ ): Buffer {
522
+ return dispatch(symbols, arch, platform);
523
+ }
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@retrovm/nobj",
3
+ "version": "0.1.0",
4
+ "description": "Generate native linkable object files (COFF, Mach-O, ELF64) from TypeScript/Bun — zero native dependencies",
5
+ "license": "MIT",
6
+ "author": "Juan Carlos González Amestoy",
7
+ "keywords": ["native", "object", "coff", "macho", "elf", "linker", "bun", "embed"],
8
+ "exports": {
9
+ ".": "./nobj.ts"
10
+ },
11
+ "files": [
12
+ "nobj.ts",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "devDependencies": {
17
+ "@types/bun": "latest"
18
+ },
19
+ "peerDependencies": {
20
+ "typescript": "^5"
21
+ }
22
+ }