@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.
- package/LICENSE +21 -0
- package/README.md +135 -0
- package/nobj.ts +523 -0
- 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
|
+
}
|