Skip to content

feat(native): support standalone HTTP server binaries#148

Merged
humancto merged 6 commits into
mainfrom
feat/http-server-standalone-binary
May 3, 2026
Merged

feat(native): support standalone HTTP server binaries#148
humancto merged 6 commits into
mainfrom
feat/http-server-standalone-binary

Conversation

@humancto
Copy link
Copy Markdown
Owner

@humancto humancto commented May 3, 2026

Summary

  • Adds an embedded source-runtime entrypoint so generated binaries can run interpreter-only features like @server in-process without shelling out to forge.
  • Updates forge build --native to emit a standalone source-runtime wrapper when libforge_lang.a is available, while preserving CLI launcher fallback when it is not.
  • Keeps --aot honest as bytecode/VM-only and adds guidance for decorated server programs to use --native.
  • Documents the source-runtime tradeoff in the changelog and adds skip-safe native server smoke coverage.

Roadmap

  • Advances ROADMAP.md: HTTP servers work as standalone binaries.
  • This is explicitly source-runtime standalone, not true native route-handler codegen; startup/performance/cross-compilation remain separate roadmap items.

Test plan

  • cargo fmt -- --check
  • cargo test
  • cargo clippy --all-targets -- -A clippy::approx_constant -A clippy::result_large_err -A clippy::only_used_in_recursion -A clippy::len_zero
  • cargo run -- --allow-run test tests/
  • cargo audit
  • cargo build --lib
  • Manual smoke: FORGE_LIB_DIR=target/debug cargo run -- build --native .tmp_native_server_smoke.fg, run generated binary, curl /ping

Made with Cursor

Summary by CodeRabbit

  • New Features

    • forge build --native can produce a single standalone executable that embeds Forge source and runs without the CLI.
    • Added --allow-run to gate shell/run permissions for native builds.
    • Clearer guidance when AOT builds reject decorator-driven servers (hint to use --native).
  • Documentation

    • Planning document for standalone HTTP server binaries added.
    • CHANGELOG updated with new native build behavior.
  • Tests

    • Added unit/integration coverage and a Unix-gated end-to-end smoke test for standalone servers.

Archith and others added 5 commits May 3, 2026 10:19
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 3, 2026

📝 Walkthrough

Walkthrough

Adds a standalone-source execution path for Forge: a C-ABI entrypoint to run embedded Forge source, an embedded runtime module, and native build changes to emit C wrappers that link to libforge_lang.a (Unix-only). CLI --allow-run is added and tests + docs updated.

Changes

Standalone HTTP Server Binaries

Layer / File(s) Summary
Data / Types
src/native.rs
Adds NativeRuntimeKind and NativeBuildOutput to represent native build results.
Embedded Runtime Module
src/runtime/embedded.rs, src/runtime/mod.rs
New EmbeddedSourceConfig and functions execute_source_standalone / execute_source_on_current_runtime to parse source, run the interpreter with deferred host runtime, extract a RuntimePlan, and launch the host runtime.
C FFI Entrypoint
src/lib.rs
Adds pub unsafe extern "C" fn forge_execute_source(...) -> i32 that validates pointer/length, decodes UTF‑8 inputs, invokes embedded execution, and wraps execution with panic::catch_unwind(AssertUnwindSafe(...)), returning 0/1 status.
Native Wrapper Generation
src/native.rs
Introduces build_standalone_source (Unix-only) and standalone_source_c_source to emit a temporary C wrapper embedding FORGE_SOURCE, FORGE_SOURCE_PATH, and FORGE_ALLOW_RUN, compiling it linked against libforge_lang.a.
Native Builder Refactor
src/native.rs
build_native_launcher now takes allow_run: bool and returns NativeBuildOutput; selects standalone-source path when libforge_lang.a is discoverable, otherwise falls back to CLI launcher.
CLI Integration
src/main.rs
Adds --allow-run (requires --native, conflicts with --aot), threads flag into compile_to_native_launchernative::build_native_launcher, and augments AOT incompatibility hint to suggest --native. Includes unit test for CLI gating.
Tests & Documentation
.planning/http-servers-standalone-binaries.plan.md, CHANGELOG.md, src/native.rs tests
Adds planning doc and changelog entry; tests updated to consume NativeBuildOutput, assert generated C wrapper embeds source and FORGE_ALLOW_RUN, and a Unix-gated E2E smoke test that builds a standalone server and polls /ping (skips when static lib missing).

Sequence Diagram

sequenceDiagram
    participant Binary as Standalone Binary
    participant FFI as forge_execute_source<br/>(C FFI)
    participant Runtime as Embedded Runtime
    participant Interp as Interpreter
    participant Host as Host Runtime
    participant Server as HTTP Server

    Binary->>FFI: call(source_ptr, source_len, path_ptr, path_len, allow_run)
    FFI->>FFI: validate pointers & lengths\ndecode UTF-8 inputs
    FFI->>Runtime: execute_source_standalone(source, config)
    Runtime->>Runtime: build Tokio runtime\nrun async execution
    Runtime->>Interp: parse & interpret with deferred host
    Interp->>Interp: produce RuntimePlan from metadata
    Interp->>Host: runtime::host::launch(plan)
    Host->>Server: start HTTP server (e.g. `@server`)
    Server-->>Binary: accept requests (e.g. /ping)
    Runtime-->>FFI: return Result
    FFI-->>Binary: exit status (0/1)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

I knit bytes into burrows, a tiny binary song,
Embed the source snug where runners belong.
No CLI hopping, just a quiet startup hum,
Servers wake, pings hop — the rabbit’s work is done. 🐰

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main feature: adding support for standalone HTTP server binaries via the native build path.
Description check ✅ Passed The description covers the core changes, roadmap advancement, and testing performed; it matches the template sections (Summary, Changes via bullet points, Testing via checklist) and is substantive.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/http-server-standalone-binary

Review rate limit: 6/10 reviews remaining, refill in 23 minutes and 52 seconds.

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

Co-authored-by: Cursor <cursoragent@cursor.com>
Copy link
Copy Markdown

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

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: a80cb5f376

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/native.rs Outdated
Comment on lines +12 to +14
if let Some(lib_dir) = find_libforge_dir() {
return build_standalone_source(source, source_path, allow_run, &lib_dir)
.map(|path| NativeBuildOutput::standalone(path));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Fall back to CLI launcher when standalone link fails

When find_libforge_dir() returns a path, build_native_launcher now returns the result of build_standalone_source directly, so any link failure (e.g., incompatible libforge_lang.a, missing system linker inputs, or bad FORGE_LIB_DIR) hard-fails the build instead of producing the previous launcher binary. This is a regression for forge build --native: users who previously got a working launcher now get an error solely because a static library is discoverable but unusable. The function should attempt standalone first, then fall back to compile_launcher(...) on standalone build errors.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

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

Inline comments:
In `@CHANGELOG.md`:
- Line 12: The changelog entry for "Standalone source-runtime native binaries
for Forge servers" is missing the PR reference; update the bullet so it follows
the required format by appending the PR suffix like " ([`#148`](link))" to the end
of that line under the [Unreleased] section (i.e., modify the existing line that
begins "Standalone source-runtime native binaries for Forge servers" to include
the PR reference and URL).

In `@src/main.rs`:
- Around line 347-353: The code is passing a combined flag (cli.allow_run ||
build_allow_run) into compile_to_native_launcher which lets the top-level
--allow-run incorrectly override the Build subcommand's requires/conflicts
semantics; change the call to pass only the build-local flag (build_allow_run)
to compile_to_native_launcher so only the Build subcommand's allow_run controls
embedding runtime shell permissions, leaving cli.allow_run to affect only the
top-level process.

In `@src/native.rs`:
- Around line 678-704: Currently the test binds to an ephemeral port then drops
the listener (listener/port) which allows races; instead remove the bind/drop
handshake and implement a retry loop when launching the child server: pick a
random ephemeral port, write source using that port
(temp_root/source_path/source), build with build_standalone_source, start the
server, and if startup fails with an address-in-use/bind error retry a few times
with a freshly generated port/source pair before failing the test; reference the
listener/port variables to remove, and reuse temp_root/source_path and
build_standalone_source in the retry logic.
🪄 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 Plus

Run ID: 6dd3f7b2-cdd5-4e10-aa72-87bff3bc0aa2

📥 Commits

Reviewing files that changed from the base of the PR and between 909a81d and a80cb5f.

📒 Files selected for processing (7)
  • .planning/http-servers-standalone-binaries.plan.md
  • CHANGELOG.md
  • src/lib.rs
  • src/main.rs
  • src/native.rs
  • src/runtime/embedded.rs
  • src/runtime/mod.rs

Comment thread CHANGELOG.md

### Added

- **Standalone source-runtime native binaries for Forge servers** — `forge build --native` now links against `libforge_lang.a` when available and emits a single executable that embeds Forge source and starts interpreter-only runtime features like `@server` without shelling out to the `forge` CLI. `--aot` remains bytecode/VM-only and continues to reject decorator-driven servers with guidance to use `--native`.
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 | 🟡 Minor | ⚡ Quick win

Add the PR link suffix to this changelog entry.

This entry is missing the required ([#148](...)) reference under [Unreleased].

Suggested fix
-- **Standalone source-runtime native binaries for Forge servers** — `forge build --native` now links against `libforge_lang.a` when available and emits a single executable that embeds Forge source and starts interpreter-only runtime features like `@server` without shelling out to the `forge` CLI. `--aot` remains bytecode/VM-only and continues to reject decorator-driven servers with guidance to use `--native`.
+- **Standalone source-runtime native binaries for Forge servers** — `forge build --native` now links against `libforge_lang.a` when available and emits a single executable that embeds Forge source and starts interpreter-only runtime features like `@server` without shelling out to the `forge` CLI. `--aot` remains bytecode/VM-only and continues to reject decorator-driven servers with guidance to use `--native`. ([`#148`](https://github.com/humancto/forge-lang/pull/148))

As per coding guidelines, "Update CHANGELOG.md for every PR with user-facing changes, using the format - Description of change ([#PR](link)) under the [Unreleased] section."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- **Standalone source-runtime native binaries for Forge servers**`forge build --native` now links against `libforge_lang.a` when available and emits a single executable that embeds Forge source and starts interpreter-only runtime features like `@server` without shelling out to the `forge` CLI. `--aot` remains bytecode/VM-only and continues to reject decorator-driven servers with guidance to use `--native`.
- **Standalone source-runtime native binaries for Forge servers**`forge build --native` now links against `libforge_lang.a` when available and emits a single executable that embeds Forge source and starts interpreter-only runtime features like `@server` without shelling out to the `forge` CLI. `--aot` remains bytecode/VM-only and continues to reject decorator-driven servers with guidance to use `--native`. ([`#148`](https://github.com/humancto/forge-lang/pull/148))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CHANGELOG.md` at line 12, The changelog entry for "Standalone source-runtime
native binaries for Forge servers" is missing the PR reference; update the
bullet so it follows the required format by appending the PR suffix like "
([`#148`](link))" to the end of that line under the [Unreleased] section (i.e.,
modify the existing line that begins "Standalone source-runtime native binaries
for Forge servers" to include the PR reference and URL).

Comment thread src/main.rs
Comment on lines +347 to +353
compile_to_native_launcher(
&source,
&path_str,
&file,
strict,
cli.allow_run || build_allow_run,
);
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and examine src/main.rs
git ls-files | grep -E "^src/main\.rs$"

Repository: humancto/forge-lang

Length of output: 75


🏁 Script executed:

# Check the file size and read the relevant sections
wc -l src/main.rs

Repository: humancto/forge-lang

Length of output: 80


🏁 Script executed:

# Read the CLI struct and Build command definition
head -200 src/main.rs | tail -100

Repository: humancto/forge-lang

Length of output: 3246


🏁 Script executed:

# Read lines 151-160 to see the allow_run flag definition
sed -n '151,160p' src/main.rs

Repository: humancto/forge-lang

Length of output: 457


🏁 Script executed:

# Read lines 324-360 to see the build dispatch and the line in question
sed -n '324,360p' src/main.rs

Repository: humancto/forge-lang

Length of output: 1284


🏁 Script executed:

# Check if compile_to_native_aot takes an allow_run parameter
rg -A 5 "fn compile_to_native_aot" src/main.rs

Repository: humancto/forge-lang

Length of output: 366


🏁 Script executed:

# Check if compile_to_native_launcher takes an allow_run parameter
rg -A 5 "fn compile_to_native_launcher" src/main.rs

Repository: humancto/forge-lang

Length of output: 196


🏁 Script executed:

# Check how compile_to_bytecode is implemented and if it uses the flag
rg -A 5 "fn compile_to_bytecode" src/main.rs

Repository: humancto/forge-lang

Length of output: 364


🏁 Script executed:

# Let's also see the full context around the Build command dispatch to understand the flow
sed -n '324,355p' src/main.rs

Repository: humancto/forge-lang

Length of output: 1164


🏁 Script executed:

# Check all calls to compile_to_native_launcher to see if there are other places using it
rg "compile_to_native_launcher" src/main.rs

Repository: humancto/forge-lang

Length of output: 138


🏁 Script executed:

# Let me verify the actual behavior by checking what the top-level allow_run affects
rg -B 5 -A 10 "Some(Command::Run" src/main.rs | head -40

Repository: humancto/forge-lang

Length of output: 136


🏁 Script executed:

# Let's also check if there are any tests or documentation about this flag behavior
find . -name "*.md" -o -name "*.rs" | xargs rg "allow-run|allow_run" | head -20

Repository: humancto/forge-lang

Length of output: 1326


🏁 Script executed:

# Check the Run command to see how it uses allow_run
sed -n '1,330p' src/main.rs | grep -A 20 "Some(Command::Run"

Repository: humancto/forge-lang

Length of output: 1092


🏁 Script executed:

# Find where permissions::set_allow_run is called and how it's used
rg -B 3 -A 3 "permissions::set_allow_run" src/main.rs

Repository: humancto/forge-lang

Length of output: 393


🏁 Script executed:

# Check if there are any comments or documentation about the semantic difference
rg -B 5 "cli.allow_run" src/main.rs

Repository: humancto/forge-lang

Length of output: 630


🏁 Script executed:

# Let me trace the exact flow - check if compile_to_native_launcher is called anywhere else
rg -n "compile_to_native_launcher" src/main.rs

Repository: humancto/forge-lang

Length of output: 146


🏁 Script executed:

# Verify the exact structure and branching logic around the build dispatch
sed -n '340,360p' src/main.rs

Repository: humancto/forge-lang

Length of output: 728


🏁 Script executed:

# Check what the Build command allow_run constraint actually does in Clap
sed -n '108,118p' src/main.rs

Repository: humancto/forge-lang

Length of output: 397


Remove the top-level --allow-run bypass in the build dispatch.

The Build subcommand's allow_run field explicitly declares requires = "native" and conflicts_with = "aot", establishing that shell execution permission embedding is only valid for native builds. However, cli.allow_run || build_allow_run allows the top-level flag to bypass this constraint—a user can pass forge --allow-run build --aot app.fg and the top-level permission will be OR'd into the decision.

While the aot branch doesn't use the allow_run parameter (so there's no functional bug), this violates the documented semantic contract. The top-level --allow-run controls runtime permissions for the forge process itself, whereas the build subcommand's --allow-run controls whether to embed permissions in the compiled binary—these are separate concerns and should not be conflated. Only the build-local flag should apply to the build output.

Suggested fix
                 compile_to_native_launcher(
                     &source,
                     &path_str,
                     &file,
                     strict,
-                    cli.allow_run || build_allow_run,
+                    build_allow_run,
                 );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
compile_to_native_launcher(
&source,
&path_str,
&file,
strict,
cli.allow_run || build_allow_run,
);
compile_to_native_launcher(
&source,
&path_str,
&file,
strict,
build_allow_run,
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.rs` around lines 347 - 353, The code is passing a combined flag
(cli.allow_run || build_allow_run) into compile_to_native_launcher which lets
the top-level --allow-run incorrectly override the Build subcommand's
requires/conflicts semantics; change the call to pass only the build-local flag
(build_allow_run) to compile_to_native_launcher so only the Build subcommand's
allow_run controls embedding runtime shell permissions, leaving cli.allow_run to
affect only the top-level process.

Comment thread src/native.rs
Comment on lines +678 to +704
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind ephemeral port");
let port = listener.local_addr().unwrap().port();
drop(listener);

let temp_root = std::env::temp_dir().join(format!(
"forge-native-server-test-{}",
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&temp_root).unwrap();
let source_path = temp_root.join("server.fg");
let source = format!(
r#"
@server(port: {port})

@get("/ping")
fn ping() -> Json {{
return {{ ok: true }}
}}
"#
);
std::fs::write(&source_path, &source).unwrap();

let output_path =
build_standalone_source(&source, &source_path, false, &lib_dir).expect("build server");
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 | 🟡 Minor | ⚡ Quick win

Avoid the bind/drop/rebind port handoff in this smoke test.

The test releases the ephemeral port before the child process binds it, so another process can steal that port and make this test fail nondeterministically in CI. Retrying with a freshly generated source/port pair on startup failure would make the smoke test much less flaky.

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

In `@src/native.rs` around lines 678 - 704, Currently the test binds to an
ephemeral port then drops the listener (listener/port) which allows races;
instead remove the bind/drop handshake and implement a retry loop when launching
the child server: pick a random ephemeral port, write source using that port
(temp_root/source_path/source), build with build_standalone_source, start the
server, and if startup fails with an address-in-use/bind error retry a few times
with a freshly generated port/source pair before failing the test; reference the
listener/port variables to remove, and reuse temp_root/source_path and
build_standalone_source in the retry logic.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/main.rs (1)

347-353: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use only build-local --allow-run when embedding native permissions.

Line 352 still ORs top-level and build-local flags. That bypasses the build --allow-run scoping and can unintentionally bake shell permission into produced native binaries.

Suggested fix
                 compile_to_native_launcher(
                     &source,
                     &path_str,
                     &file,
                     strict,
-                    cli.allow_run || build_allow_run,
+                    build_allow_run,
                 );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.rs` around lines 347 - 353, The call to compile_to_native_launcher
is incorrectly combining top-level and build-local run permissions
(cli.allow_run || build_allow_run), which can leak top-level --allow-run into
embedded native binaries; update the call so it only uses the build-local flag
(build_allow_run) when embedding native permissions by replacing the OR
expression with build_allow_run so compile_to_native_launcher(...) receives the
scoped build-local value only.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/main.rs`:
- Around line 347-353: The call to compile_to_native_launcher is incorrectly
combining top-level and build-local run permissions (cli.allow_run ||
build_allow_run), which can leak top-level --allow-run into embedded native
binaries; update the call so it only uses the build-local flag (build_allow_run)
when embedding native permissions by replacing the OR expression with
build_allow_run so compile_to_native_launcher(...) receives the scoped
build-local value only.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: eabe6aae-80af-4c31-948b-1ae098b07c59

📥 Commits

Reviewing files that changed from the base of the PR and between a80cb5f and 7561672.

📒 Files selected for processing (3)
  • src/main.rs
  • src/native.rs
  • src/runtime/embedded.rs
✅ Files skipped from review due to trivial changes (1)
  • src/native.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/runtime/embedded.rs

@humancto humancto merged commit 3e1952e into main May 3, 2026
12 checks passed
@humancto humancto deleted the feat/http-server-standalone-binary branch May 3, 2026 17:49
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.

1 participant