Skip to content

Adds a Zig stdlib compatibility layer so zpdf builds on both Zig 0.15.x and Zig 0.16.0.#8

Merged
Lulzx merged 4 commits into
Lulzx:mainfrom
trvon:main
Jul 20, 2026
Merged

Adds a Zig stdlib compatibility layer so zpdf builds on both Zig 0.15.x and Zig 0.16.0.#8
Lulzx merged 4 commits into
Lulzx:mainfrom
trvon:main

Conversation

@trvon

@trvon trvon commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

This PR keeps zpdf’s public API unchanged unchanged and routes version-specific stdlib differences through src/compat.zig.

Summary by CodeRabbit

  • New Features

    • Improved Zig version compatibility across the build and runtime, with added compatibility-focused testing.
  • Tests

    • Expanded unit and integration tests to validate consistent writer behavior and compatibility utilities (I/O, timing, process execution).
  • Refactor

    • Standardized stdout/stderr, file operations, timing, and subprocess handling through a compatibility layer across CLI, benchmarks, and extraction paths.
  • Chores

    • Added a CI workflow running tests and builds across multiple Zig versions (native and WebAssembly).

…hanges, including ArrayList writers, process entrypoints, file I/O, timers, and child process execution
Copilot AI review requested due to automatic review settings April 29, 2026 15:08
@coderabbitai

coderabbitai Bot commented Apr 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

A new compatibility module centralizes Zig standard-library differences for allocation, arguments, I/O, files, timing, and processes. Application and test call sites use these helpers, compatibility tests are added to the build, and CI validates Zig 0.15.2 and 0.16.0.

Changes

Compatibility and validation

Layer / File(s) Summary
Compatibility layer
src/compat.zig
Adds cross-version allocator, entrypoint, writer, filesystem, I/O, timestamp, and process helpers with unit tests.
Compatibility test wiring
build.zig
Adds and runs the src/compat.zig unit test suite with the overall test step.
CI matrix
.github/workflows/ci.yml
Runs tests, native builds, and WebAssembly builds across Zig 0.15.2 and 0.16.0.

Runtime integration

Layer / File(s) Summary
Entrypoints and process utilities
gen_test.zig, src/main.zig, src/bench.zig
Uses compatibility entrypoint, allocator, I/O, timestamp, file, and ignored-process helpers.
File loading and extraction
src/root.zig, src/capi.zig, src/wapi.zig
Routes platform-specific file loading and extraction buffers through compatibility helpers, including deferred buffer cleanup.
ArrayList writer migration
src/encoding.zig, src/integration_test.zig, src/testpdf.zig
Replaces direct ArrayList writers with compat.arrayListWriter at decoding, integration-test, and PDF-generation call sites.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • Lulzx/zpdf#5: Both changes modify the text-extraction pipeline and its ArrayList writer flow.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: a Zig stdlib compatibility layer targeting Zig 0.15.x and 0.16.0 support.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/bench.zig (1)

127-133: Return value from runIgnored is discarded—consider checking for execution failures.

The exit code returned by compat.runIgnored is assigned to _ and discarded. If mutool exits with a non-zero code (indicating a failure), the benchmark will still report timing results that may be misleading. Consider checking the exit code for validity.

Proposed fix
 fn benchMutool(allocator: std.mem.Allocator, pdf_path: []const u8) !f64 {
     const start = compat.nanoTimestamp();

-    _ = try compat.runIgnored(&.{ "mutool", "draw", "-F", "txt", "-o", "/dev/null", pdf_path }, allocator);
+    const exit_code = try compat.runIgnored(&.{ "mutool", "draw", "-F", "txt", "-o", "/dev/null", pdf_path }, allocator);
+    if (exit_code != 0) return error.MutoolFailed;

     return `@floatFromInt`(compat.nanoTimestamp() - start);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bench.zig` around lines 127 - 133, The benchmark discards the exit code
from compat.runIgnored in benchMutool so failures of the mutool process are
ignored; change the call to capture the return value (e.g., exitCode := try
compat.runIgnored(..., allocator)), then check if exitCode != 0 and propagate or
return an error (or log/report the non-zero exit) instead of ignoring it so the
benchmark doesn't report timings for failed runs; update benchMutool to handle
non-zero exit codes from compat.runIgnored accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/compat.zig`:
- Around line 227-247: In runIgnored replace the direct access to term.Exited
(from the comptime `@hasDecl` branch where you call child.spawnAndWait()) with a
safe match like the other branch: inspect the returned termination value (the
variable term) using a switch on its variants, return the exit code when term is
.Exited and return a fallback (e.g., 255) for non-exit cases (e.g., .Signaled),
so accessing term.Exited cannot panic; update the branch using
child.spawnAndWait() to mirror the switch logic used in the other branch.
- Around line 168-200: In mmapFileReadOnlyCwd fix the incorrect prot argument in
the else branch: replace the struct literal used for the prot parameter with the
integer constant from std.posix.PROT (use std.posix.PROT.READ, matching the if
branch) so the call to std.posix.mmap in the else branch uses the same prot type
as the if branch; update the mmap invocation that currently passes .{ .READ =
true } to pass std.posix.PROT.READ and keep the rest of the parameters (flags,
file.handle, offset) unchanged.

---

Nitpick comments:
In `@src/bench.zig`:
- Around line 127-133: The benchmark discards the exit code from
compat.runIgnored in benchMutool so failures of the mutool process are ignored;
change the call to capture the return value (e.g., exitCode := try
compat.runIgnored(..., allocator)), then check if exitCode != 0 and propagate or
return an error (or log/report the non-zero exit) instead of ignoring it so the
benchmark doesn't report timings for failed runs; update benchMutool to handle
non-zero exit codes from compat.runIgnored accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 61c541e8-6787-4d32-b14c-35f5ad7701a1

📥 Commits

Reviewing files that changed from the base of the PR and between 5eba7ad and e1309aa.

📒 Files selected for processing (11)
  • build.zig
  • gen_test.zig
  • src/bench.zig
  • src/capi.zig
  • src/compat.zig
  • src/encoding.zig
  • src/integration_test.zig
  • src/main.zig
  • src/root.zig
  • src/testpdf.zig
  • src/wapi.zig

Comment thread src/compat.zig
Comment on lines +168 to +200
pub fn mmapFileReadOnlyCwd(allocator: std.mem.Allocator, path: []const u8) ![]align(std.heap.page_size_min) u8 {
if (comptime has_legacy_fs_file) {
const file = try std.fs.cwd().openFile(path, .{});
defer file.close();

const stat = try file.stat();
return std.posix.mmap(
null,
stat.size,
std.posix.PROT.READ,
.{ .TYPE = .PRIVATE },
file.handle,
0,
);
} else {
var threaded: std.Io.Threaded = .init(allocator, .{});
defer threaded.deinit();
const io = threaded.io();

const file = try std.Io.Dir.cwd().openFile(io, path, .{});
defer file.close(io);

const stat = try file.stat(io);
return std.posix.mmap(
null,
stat.size,
.{ .READ = true },
.{ .TYPE = .PRIVATE },
file.handle,
0,
);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if the code compiles by examining how PROT is used elsewhere in Zig stdlib or the codebase
rg -n "posix\.PROT" --type zig -C2

Repository: Lulzx/zpdf

Length of output: 283


🏁 Script executed:

sed -n '190,200p' src/compat.zig

Repository: Lulzx/zpdf

Length of output: 291


🏁 Script executed:

rg -n "mmap" --type zig -B2 -A6

Repository: Lulzx/zpdf

Length of output: 2927


🏁 Script executed:

rg -n "PROT\s*=" --type zig -B2 -A5

Repository: Lulzx/zpdf

Length of output: 36


🏁 Script executed:

rg -n "fn mmap" --type zig -B2 -A10

Repository: Lulzx/zpdf

Length of output: 722


🏁 Script executed:

# Check for any type definitions or struct definitions related to PROT
rg -n "PROT|prot" --type zig | head -30

Repository: Lulzx/zpdf

Length of output: 106


🌐 Web query:

Zig std.posix.mmap prot parameter documentation

💡 Result:

The prot parameter of std.posix.mmap specifies the memory access protection for the mapped region. It is a u32 (or c_uint) bitfield with standard POSIX constants available in std.posix.PROT, which are re-exported from the underlying system definitions (e.g., std.c.PROT or OS-specific like std.os.linux.PROT). Common constants: - std.posix.PROT.READ (0x1): read access - std.posix.PROT.WRITE (0x2): write access - std.posix.PROT_EXEC (0x4): execute access - std.posix.PROT_NONE (0x0): no access These can be bitwise OR'd together (e.g., std.posix.PROT.READ | std.posix.PROT.WRITE). The mmap function signature is approximately: pub fn mmap(addr: ?[]align(std.mem.page_size) u8, length: usize, prot: std.posix.PROT, flags: std.posix.MAP.Flags, fd: std.posix.fd_t, offset: u64) MmapError![]align(std.mem.page_size) u8 std.posix.mmap is a cross-platform POSIX wrapper that lowers to libc or direct syscalls, providing the standard mmap(2) semantics where prot controls page protections enforced by the OS. Note: On Linux, equivalent constants exist as std.os.linux.PROT.READ etc., but std.posix.PROT is preferred for portability. Windows does not support POSIX mmap; use OS-specific APIs.

Citations:


🌐 Web query:

Zig std.posix.PROT type definition struct flag

💡 Result:

In Zig's standard library, std.posix.PROT is an alias for std.os.system.PROT, which provides the POSIX mmap protection flags as constants. It is not a struct but a namespace containing integer constants such as PROT_NONE, PROT_READ, PROT_WRITE, PROT_EXEC. These constants are used bitwise, e.g., std.posix.PROT.READ | std.posix.PROT.WRITE. The values are platform-specific but standardized across POSIX systems: - PROT_NONE = 0 - PROT_READ = 1 - PROT_WRITE = 2 - PROT_EXEC = 4 std.posix.PROT re-exports these from the underlying system definitions (e.g., std.os.linux.PROT on Linux), ensuring portability. Examples from usage: const ptr = try std.posix.mmap(null, length, std.posix.PROT.READ | std.posix.PROT.WRITE, .{ .TYPE = .SHARED }, fd, 0);

Citations:


🏁 Script executed:

# Search for how flags/PROT types are typically defined in Zig
rg -n "TYPE.*PRIVATE" --type zig -B3 -A3

Repository: Lulzx/zpdf

Length of output: 641


Fix compile error: incorrect prot argument syntax in else branch of mmapFileReadOnlyCwd.

Line 177 correctly uses std.posix.PROT.READ (an integer constant), but line 194 incorrectly uses .{ .READ = true } (a struct literal). The prot parameter to std.posix.mmap expects an integer constant from the std.posix.PROT namespace, not a struct. The else branch will fail to compile. Change line 194 to std.posix.PROT.READ to match line 177.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/compat.zig` around lines 168 - 200, In mmapFileReadOnlyCwd fix the
incorrect prot argument in the else branch: replace the struct literal used for
the prot parameter with the integer constant from std.posix.PROT (use
std.posix.PROT.READ, matching the if branch) so the call to std.posix.mmap in
the else branch uses the same prot type as the if branch; update the mmap
invocation that currently passes .{ .READ = true } to pass std.posix.PROT.READ
and keep the rest of the parameters (flags, file.handle, offset) unchanged.

Comment thread src/compat.zig

Copilot AI 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.

Pull request overview

Adds a Zig stdlib compatibility layer so zpdf can build on both Zig 0.15.x and Zig 0.16.0 by centralizing version-specific stdlib differences in src/compat.zig.

Changes:

  • Introduces src/compat.zig with adapters for ArrayList writing, process entrypoint args handling, file I/O helpers, timestamps, and running child processes.
  • Updates CLI/tools/tests/C API/WASM API to use the compat adapters instead of version-specific stdlib calls.
  • Adds compat.zig unit tests to the build test step.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/compat.zig New compatibility layer (ArrayList writer, fs/process/timing wrappers) + unit tests
src/root.zig Routes file open/mmap and ArrayList writer usage through compat
src/main.zig Uses compat main-entry wrapper + compat stdout/stderr/file/timestamp/process helpers
src/bench.zig Uses compat main-entry wrapper + compat file size/timestamp/process helpers
src/testpdf.zig Uses compat ArrayList writer adapter for PDF generation
src/integration_test.zig Uses compat ArrayList writer adapter for extraction tests
src/encoding.zig Uses compat ArrayList writer adapter in tests; removes stray whitespace
src/capi.zig Uses compat ArrayList writer adapter for C API extraction
src/wapi.zig Uses compat ArrayList writer adapter for WASM API extraction
gen_test.zig Uses compat allocator + compat file helpers (and initializes Io when needed)
build.zig Adds compat.zig unit test artifact to the test step

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/compat.zig Outdated
child.stderr_behavior = .Ignore;
child.stdout_behavior = .Ignore;
const term = try child.spawnAndWait();
return term.Exited;

Copilot AI Apr 29, 2026

Copy link

Choose a reason for hiding this comment

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

In the legacy branch, spawnAndWait() can return termination states other than normal exit (e.g., signal). Returning term.Exited without switching on the tag can trigger a safety trap or undefined behavior. Match the non-legacy branch by switching on term and mapping non-.Exited cases to a sentinel (e.g., 255).

Suggested change
return term.Exited;
return switch (term) {
.Exited => |code| code,
else => 255,
};

Copilot uses AI. Check for mistakes.
Comment thread src/wapi.zig
Comment on lines 89 to 91
var buffer: std.ArrayList(u8) = .empty;
doc.extractText(@intCast(page_num), buffer.writer(wasm_allocator)) catch return null;
doc.extractText(@intCast(page_num), compat.arrayListWriter(&buffer, wasm_allocator)) catch return null;

Copilot AI Apr 29, 2026

Copy link

Choose a reason for hiding this comment

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

buffer is never deinitialized on the error path (the catch return null after extractText). If extractText has already appended to the list, this leaks WASM heap memory. Consider adding errdefer buffer.deinit(wasm_allocator); immediately after initializing the ArrayList (and ensuring the empty-buffer early-return path also deinitializes if capacity was allocated).

Copilot uses AI. Check for mistakes.
Comment thread src/capi.zig
@@ -56,7 +57,7 @@ export fn zpdf_extract_page(handle: ?*ZpdfDocument, page_num: c_int, out_len: *u
if (page_num < 0) return null;

var buffer: std.ArrayList(u8) = .empty;

Copilot AI Apr 29, 2026

Copy link

Choose a reason for hiding this comment

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

buffer is not deinitialized if extractText fails (catch return null). If any bytes were appended before the error, this leaks memory from c_allocator. Add an errdefer buffer.deinit(c_allocator); after initialization (and keep the current toOwnedSlice ownership transfer on success).

Suggested change
var buffer: std.ArrayList(u8) = .empty;
var buffer: std.ArrayList(u8) = .empty;
errdefer buffer.deinit(c_allocator);

Copilot uses AI. Check for mistakes.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/capi.zig`:
- Around line 60-61: The current use of errdefer for buffer.deinit(c_allocator)
won't run when doc.extractText(`@intCast`(page_num),
compat.arrayListWriter(&buffer, c_allocator)) exits via "catch return null"
because the function returns an optional, not an error union; replace errdefer
with a plain defer so buffer.deinit(c_allocator) always runs on normal returns
as well as errors, ensuring the allocation is freed even when extractText
triggers "catch return null". Ensure the defer is placed immediately after
buffer is initialized (before calling doc.extractText) so it always applies.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 438d3f5b-7bf9-48f0-98b9-e0e2f07dc444

📥 Commits

Reviewing files that changed from the base of the PR and between e1309aa and ff8d560.

📒 Files selected for processing (3)
  • src/capi.zig
  • src/compat.zig
  • src/wapi.zig
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/wapi.zig
  • src/compat.zig

Comment thread src/capi.zig Outdated

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
.github/workflows/ci.yml (1)

23-23: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Disable credential persistence in checkout action.

It is a security best practice to set persist-credentials: false when using actions/checkout. This prevents the GITHUB_TOKEN from being persisted in the local .git configuration, reducing the risk of credential leakage during the build process.

💡 Proposed fix
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v4
+        with:
+          persist-credentials: false
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml at line 23, Update the actions/checkout@v4 step to
set persist-credentials to false, preventing the GitHub token from being stored
in the repository’s Git configuration.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In @.github/workflows/ci.yml:
- Line 23: Update the actions/checkout@v4 step to set persist-credentials to
false, preventing the GitHub token from being stored in the repository’s Git
configuration.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4a8d2fae-fddb-4d89-83fc-f83d160f2709

📥 Commits

Reviewing files that changed from the base of the PR and between 67fe58b and 62f3414.

📒 Files selected for processing (1)
  • .github/workflows/ci.yml

@Lulzx
Lulzx merged commit 9271ec6 into Lulzx:main Jul 20, 2026
4 checks passed
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.

3 participants