Skip to content

cgen: use .incbin assembler directives for $embed_file, eliminate the 5MB limit#27438

Open
MagicFun1241 wants to merge 6 commits into
vlang:masterfrom
MagicFun1241:feat/incbin-embed
Open

cgen: use .incbin assembler directives for $embed_file, eliminate the 5MB limit#27438
MagicFun1241 wants to merge 6 commits into
vlang:masterfrom
MagicFun1241:feat/incbin-embed

Conversation

@MagicFun1241

@MagicFun1241 MagicFun1241 commented Jun 12, 2026

Copy link
Copy Markdown

Summary

Replace C hex array literals with GNU assembler .incbin directives for $embed_file, matching Go's //go:embed SRODATA approach. Raw bytes are written directly into .rodata via assembly, eliminating the intermediate C source text expansion.

Problem

V's current embed implementation generates C hex array literals for embedded file data. A 10MB file produces ~53MB of C source, causing:

  • 5MB practical limit — files above this trigger a warning and make compilation prohibitively slow
  • 6× source expansion — 8MB file → 48MB of C source that the C compiler must parse
  • Huge compiler RSS — 10MB embed → 1.1GB peak compiler memory
  • Slow compilation — compile time scales linearly with file size (10MB embed → 7.6s)

Approach

For GCC, Clang, MinGW, and TCC (when a system assembler is available), each embedded file is written as a .S assembly file using .incbin pointing at the original binary. The .S file uses #if defined(__APPLE__) / #elif defined(_WIN32) / #else to emit the correct section directives for each platform (Mach-O __TEXT,__const, COFF .rdata, ELF .rodata) and handle macOS's extra _ C symbol prefix. ELF-only directives (.type, .size) are excluded on Windows.

The C hex array path remains as fallback for MSVC, Windows cross-compilation, -o .c output, .o output mode, wasm targets, and build_module (-usecache) mode — zero regressions.

Compressed embeds (.zlib) keep their temp .bin files alive for .incbin to reference at assembly time, with cleanup scheduled via cleanup_files.

Symbol names use fnv1a hash-based naming (_v_embed_blob_<hash>) to avoid collisions during parallel test execution. Deduplication uses EmbeddedFile.hash() instead of struct equality to handle compressed files with random temp paths.

gen_c() return type stays strings.Builder — embed data flows through Builder struct fields (embedded_asm, embedded_o_files, embedded_temp_files), compiled to .o in gen_c() (parallel_cc) or build_c() (serial), then added to linker args via the same path as thirdparty .o files.

Design decisions

Why .incbin instead of alternatives considered

Option Pros Cons
.incbin (chosen) Zero C source expansion; compiler-agnostic; Go uses this pattern Requires assembler step
fread at runtime Simple No embedded data — breaks the contract of $embed_file
Linker objcopy --input binary No assembler needed Extra tool dependency; harder cross-compilation
Keep C hex arrays Works everywhere 6× expansion; 5MB limit; OOM on large files

Platform fallbacks

Platform / Mode Path Reason
GCC, Clang, MinGW .incbin Native assembler handles .S files
TCC (with clang/gcc/cc available) .incbin Falls back to system assembler for .S files
TCC (no system assembler) C hex arrays Raw as excluded: no -c flag, no #if preprocessing
MSVC C hex arrays MSVC assembler doesn't support .incbin
Windows cross-compile C hex arrays Cross-linker can't consume host .o files
wasm32 / wasm32_emscripten C hex arrays WASM object format incompatible with ELF/Mach-O .S
build_module (-usecache) C hex arrays Separate .o can't be merged into cached module .o
-o .c / -o .o / stdout C hex arrays No binary output to link embed .o into

Windows / COFF handling

The .S assembly uses #elif defined(_WIN32) to emit .section .rdata (COFF read-only data) instead of .section .rodata (ELF), and skips .type/.size directives that COFF assemblers reject.

TCC handling

Two-level fallback:

  1. Codegen time (should_use_incbin_embed): check if clang/gcc/cc is available. If yes → .incbin. If no → C hex arrays.
  2. Build time (compile_embedded_asm_files): if TCC reaches this point, find the system assembler. If not found → verror() with clear message.

Linux cross-architecture

When cross-compiling Linux x86_64 → Linux arm64 (same OS, different arch), the .S file must be assembled with -target matching the cross-compilation triple. The condition now checks host_os != .linux || host_arch != b.pref.arch.

Deduplication

EmbeddedFile deduplication uses hash() (based on apath, compression_type, is_compressed, len) instead of struct equality, because compressed_temp_path contains a random ULID that would prevent deduplication of the same compressed file embedded twice.

Hash-based symbol naming

Parallel test execution (v test) compiles multiple binaries simultaneously. Sequential names like _v_embed_blob_0 collide; hash-based names (_v_embed_blob_<fnv1a_hash>) ensure uniqueness per file path.

Parallel cc ordering

parallel_cc() runs inside gen_c(), before build_c(). Embed .o files are compiled in-place in gen_c() with a guard (embedded_o_files.len == 0) to prevent double-compilation in build_c().

Performance comparison

macOS, Apple clang 17, averaged over 3 runs:

Size Method Compile (s) Compiler RSS (MB) C source (MB) Binary (MB)
1KB hex 1.10 105 0.40 0.20
1KB incbin 0.51 98 ~0 0.20
100KB hex 1.18 112 0.91 0.31
100KB incbin 0.48 97 ~0 0.31
1MB hex 1.78 206 5.70 1.21
1MB incbin 0.48 98 ~0 1.21
5MB hex 4.39 623 26.95 5.24
5MB incbin 0.49 102 ~0 5.24
10MB hex 7.61 1,143 53.52 10.28
10MB incbin 0.50 107 ~0 10.28
25MB hex impossible OOM ~134
25MB incbin 0.51 122 ~0 25.40

…he 5MB limit and 6x C-source expansion

Replace C hex array literals (0x2f,0x2a,...) with GNU assembler .incbin
directives that write raw bytes directly into .rodata/.S files, similar to
Go's //go:embed SRODATA approach.

For GCC/Clang/TCC targets, each embedded file is written as a .S assembly
file with .incbin pointing at the original binary. The .S file uses
#if defined(__APPLE__) to emit the correct Mach-O (.section __TEXT,__const)
or ELF (.section .rodata) directives, and handles macOS's extra _ C symbol
prefix. Compressed embeds keep their temp .bin files alive for .incbin to
reference at assembly time.

The C hex array path remains as fallback for MSVC, Windows cross-compilation,
-o .c output, and .o output mode, ensuring zero regressions.

Embedded file .S symbols use fnv1a hash-based naming (_v_embed_blob_<hash>)
to avoid collisions during parallel test execution.

The gen_c() return type stays strings.Builder — embed data flows through
Builder struct fields (embedded_asm, embedded_o_files, embedded_temp_files)
and is compiled to .o files in gen_c() (parallel_cc) or build_c() (serial),
then added to linker args via the same path as thirdparty .o files.

Benchmark (macOS, Apple clang 17):
  - 10MB embed: 0.5s vs 7.6s compile (15x faster)
  - 10MB embed: 107MB vs 1143MB peak compiler RSS (10x less)
  - 10MB embed: ~0 vs 53.5MB C source (eliminated)
  - 25MB embed: 0.5s compile (was impossible with C hex)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c27d1b04d7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread vlib/v/gen/c/embed.v Outdated
Wasm uses a different object format (WASM, not ELF/Mach-O). Our .S files
contain ELF/Mach-O section directives that would either fail to assemble
or produce host-arch .o files that can'\''t link into the wasm output.
The C hex array fallback is correct for these targets.

Also adds a safety check in compile_embedded_asm_files() that verror()s
if wasm data somehow reaches the .S assembly path.

Found by Codex review on PR vlang#27438.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@MagicFun1241

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0b56b0e172

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread vlib/v/gen/c/embed.v
Comment thread vlib/v/gen/c/embed.v
…sembly

Three fixes from Codex review (PR vlang#27438):

1. wasm32 targets: .S files with ELF/Mach-O directives produce host-arch
   .o files incompatible with wasm. Already disabled .emcc from allow-list
   in prior commit; now also adds a verror() safety check in
   compile_embedded_asm_files().

2. build_module (-usecache): V compiles each module as a standalone .o.
   Embed .o files are separate and cannot be merged into the cached module
   .o, causing unresolved _v_embed_blob_* symbols at final link time.
   Disable incbin for build_mode == .build_module, with verror() safety check.

3. Windows/COFF: .type and .size are ELF-specific directives rejected by
   COFF assemblers (clang -target x86_64-w64-windows-gnu). Add
   #elif defined(_WIN32) branch using .section .rdata without .type/.size.
   The .size guard also excludes _WIN32.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@MagicFun1241

Copy link
Copy Markdown
Author

@codex review now

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5a5e85bb6f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread vlib/v/builder/cc.v Outdated
Comment thread vlib/v/gen/c/embed.v Outdated
…\` from assembler candidates

Two fixes from Codex review (PR vlang#27438):

1. Linux cross-compile arch mismatch: the previous condition only checked
   host_os != .linux, missing the case where both are Linux but target arch
   differs (e.g. x86_64 host → arm64 target). Without -target, the assembler
   produces a host-arch .o that fails to link. Now also checks host_arch vs
   pref.arch, adding -target whenever they differ.

2. Raw \`as\` removed from assembler candidate lists: GNU \`as\` doesn't
   preprocess #if directives in .S files and has no -c flag, so it can't
   assemble our generated .S files. Removed from both
   find_system_assembler() and should_use_incbin_embed() TCC fallback.
   Only clang/gcc/cc are valid candidates (they preprocess and accept -c).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@MagicFun1241

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e799c94164

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread vlib/v/gen/c/embed.v
… equality

Two embeds of the same compressed file each generate a random ULID for
compressed_temp_path, so struct equality (!in) fails to deduplicate them,
producing duplicate case labels and index entries in generated C.

Fix: deduplicate by EmbeddedFile.hash() which excludes compressed_temp_path,
so the first embed keeps its temp path and subsequent duplicates are skipped.

Found by Codex review on PR vlang#27438.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@MagicFun1241

Copy link
Copy Markdown
Author

@codex review

@JalonSolov

Copy link
Copy Markdown
Collaborator

Instead of writing a .s file, seems like the compiler should be able to directly write a .o file. Shouldn't be that hard to do, and no "system assembler" or other external dependency needs to be added to the toolchain. There aren't that many formats that would need to be supported.

@MagicFun1241

MagicFun1241 commented Jun 12, 2026

Copy link
Copy Markdown
Author

Yes. Might be an option

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f83697bc1b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread vlib/v/gen/c/embed.v Outdated
… parallel-build races

When two V processes embed the same file concurrently (parallel v test),
both would write to the same _v_embed_blob_<hash>.S/.o in the shared
vtmp_dir, causing race conditions. Add a per-build random ULID suffix
to the filenames so each build writes to unique paths.

Symbol names remain deterministic (hash-based) — only the filenames
are randomized. The Gen struct gains an embed_build_id field initialized
with rand.ulid() and propagated through GenOutput.

Found by Codex review on PR vlang#27438.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@MagicFun1241

Copy link
Copy Markdown
Author

@JalonSolov I can try to create different PR with direct .o writing approach but I think there will be near 200-300 LOC per format. Is it acceptable?

@MagicFun1241

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 57ef01b2a0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +157 to +159
// Note: embedded .o files are NOT added to ofiles here. They are already
// included via ccoptions.o_args (set up in cc() during the parallel_cc
// str_args generation), which get_linker_args() picks up.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Link embedded objects in parallel-cc builds

When -parallel-cc is used with $embed_file on an incbin-enabled target, the generated C references _v_embed_blob_*, but the compiled embed .o files are never added to the final alink command. I checked get_linker_args() in vlib/v/builder/cc.v:1152-1165; it only returns linker flags/ldflags, not ccoptions.o_args, so the assumption in this comment is false and the link step will fail with unresolved embed symbols. Add b.embedded_o_files to ofiles or directly to alink before linking.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codex .o files reach the parallel-cc link command via ccoptions.o_args, which is part of compile_args.
Link command seems to use scompile_args_for_linker, which includes o_args.

Verified with -parallel-cc -cc cc:

'cc' ... '/tmp/v_501/_v_embed_blob_10850619718018418698_01KTYRY5K11Y2HV27FFFRFRTE1.o' ... -o '.../par_bin' '.../out_0.o' ... '.../out_x.o' -lgc -ldl -lpthread

Embed .o appears in both the per-file cc_cmd and the final link_cmd. The comment is correct

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JalonSolov

JalonSolov commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

I don't think 200-300 loc per format (there aren't that many.. ELF, PE, etc.) in the compiler will be that noticeable, as long as it makes this process simpler/easier.

Besides, a large amount of the code should be sharable between the formats... the handling of the bytes in the file being embedded. The main thing that will be different will be the name of the output (.o vs .obj), and the "scaffolding"... the bits that make it a linkable object, rather than just a binary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants