Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: CI

on:
push:
branches: [main]
pull_request:

env:
NSGI_APP: host/fixture.rb
NSGI_LIB: lib

jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4

- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4"

- uses: dtolnay/rust-toolchain@stable

- name: Build
run: cargo build --workspace

- name: Run mock host
run: cargo run -p nsgi-mock-host

- name: Memory check (valgrind)
if: runner.os == 'Linux'
run: |
sudo apt-get update && sudo apt-get install -y valgrind
# memcheck for invalid reads/writes across the ABI (use-after-free,
# out-of-bounds in the response arena, dangling request views).
# Leak checking is off because the eternal embedded VM's reachable
# memory is noise, and undef-value errors are off because CRuby's
# conservative GC scans raw stack words by design, producing tens
# of thousands of false positives.
NSGI_TEST_INTERLEAVE_MS=10000 valgrind --error-exitcode=1 --leak-check=no \
--undef-value-errors=no ./target/debug/nsgi-mock-host

- name: Memory check (leaks)
if: runner.os == 'macOS'
run: leaks --atExit -- ./target/debug/nsgi-mock-host
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/target/
*.gem
.DS_Store
46 changes: 46 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# nsgi-ruby

NSGI-compliant Ruby bridge: a Rust `cdylib` (`ext/nsgi`) embeds CRuby behind the
`nsgi_handle`/`nsgi_free_response` C ABI defined by the `nsgi` crate. Protocol and
implementation are deliberately loosely coupled (PSGI-style, not Rack-style).

## Commands

```sh
cargo build # build the cdylib + mock host
NSGI_APP=host/fixture.rb NSGI_LIB=lib cargo run -p nsgi-mock-host # end-to-end verification
NSGI_APP=host/fixture.rb NSGI_LIB=lib leaks --atExit -- ./target/debug/nsgi-mock-host # macOS leak check
```

`NSGI_DEBUG=1` prints Ruby exceptions that would otherwise become silent 500s.

## Layout

- `ext/nsgi/src/lib.rs`: the whole native bridge: VM boot, job queue + wake pipe,
`NSGI::Native` callbacks (lazy zero-copy request accessors guarded by a live-request
registry, per-request processing), single-malloc response arena, static-500/503 fault
containment. Read its module docs first.
- `lib/`: pure-Ruby side: `NSGI.__run` reactor + `NSGI::Scheduler` (minimal
`Fiber::Scheduler`; fiber-per-request), `NSGI.__dispatch`, lazy `NSGI::Request`.
Loaded by the embedded VM from `NSGI_LIB`, so Ruby-only changes don't need a rebuild.
- `SPEC.md`: the normative app contract (loosely coupled from this implementation).
- `host/`: mock NSGI host (dlopen + concurrent calls); `host/fixture.rb` is its test app.
Covers error/stash/stale/buffer-body paths and asserts fiber interleaving by wall time
(`NSGI_TEST_INTERLEAVE_MS` loosens the budget for slow environments).
- `examples/hello.rb`: minimal example app (keep it pristine).

## Hard-won facts

- Booting the embedded VM requires the full `ruby_options("-e", "0")` + `ruby_exec_node`
sequence after `ruby_setup()`: bare `ruby_setup()` never runs `rb_call_builtin_inits`,
leaving Ruby-defined core methods (`Kernel#warn`, `Kernel#Integer`, …) undefined with
confusing `undefined method '#<Symbol:0x…>'` errors.
- All Ruby API calls must happen on the VM thread (Ruby's main thread). Host threads only
enqueue jobs and block on a condvar.
- Response memory must be copied out of Ruby strings (GC compaction moves embedded
strings); never let `RSTRING_PTR` cross the ABI.
- Upstream-gap findings about `IO::Buffer` (experimental) get filed as
`upstream/ruby`-labeled issues, not worked around silently.
- Ruby-side request handles must be never-reused dispatch ids, not job pointers: Arc
allocations get reused across requests (ABA), which would let a stashed Request alias
a newer request's memory.
262 changes: 262 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[workspace]
resolver = "3"
members = ["ext/nsgi", "host"]

[workspace.package]
edition = "2024"
license = "MIT"
repository = "https://github.com/nsgi-org/ruby"

[workspace.dependencies]
nsgi = "0.1.0"
Loading
Loading