Skip to content

feat(ruby): Ruby language bindings - #5178

Draft
omarqureshi wants to merge 7 commits into
aws:mainfrom
omarqureshi:ruby-language-bindings
Draft

feat(ruby): Ruby language bindings#5178
omarqureshi wants to merge 7 commits into
aws:mainfrom
omarqureshi:ruby-language-bindings

Conversation

@omarqureshi

@omarqureshi omarqureshi commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Objective

Introduces comprehensive Ruby language bindings for the jsii ecosystem, enabling AWS CDK (and other jsii-based libraries) to be packaged as gems and consumed idiomatically from Ruby. It adds the code-generation target (jsii-pacmak) and the core Ruby runtime (@jsii/ruby-runtime), with strict type-safety, careful object lifecycle management, and thread-safe IPC with the jsii Node kernel.

Implements RFC 0935: Ruby language bindings. The change is strictly additive — no breaking changes to existing languages and no assembly-format change beyond an additive targets.ruby schema extension.

Warning

CI is expected to be red on this branch, by design. Ruby code generation requires a jsii-compiler release with Ruby target support, which is not yet published. Until it is, the following failures are expected and are not indicative of broken work:

  • jsii-pacmak target-ruby snapshot test — this branch builds with the stock compiler (no Ruby support), so the committed assemblies carry no targets.ruby and generated names won't match the snapshot.
  • @jsii/ruby-runtime-test (generate.sh + rspec) — cannot generate the Ruby lib/ without the Ruby-capable compiler.
  • "Assert clean working directory" — a build would rewrite the assemblies.

Once the Ruby-capable compiler is released, the committed fixtures (assemblies with targets.ruby + the Ruby snapshot) are regenerated against it and CI goes green. End-to-end regeneration + publish has already been validated against the fork compiler.

Key Components

1. Code Generation (jsii-pacmak)

  • RubyTarget & RubyGenerator (packages/jsii-pacmak/lib/targets/ruby.ts): translate jsii types into idiomatic Ruby — class < Jsii::Object, behavioral interfaces as mixin modules, datatype structs as kwargs value-classes (< Jsii::Struct), enums as Jsii::Enum wrapper constants, async + sync methods, and property/method overrides.
  • Generated RBS signatures: every class, module, method, and struct gets a .rbs; output validates clean under rbs validate across jsii-calc and its dependency closure.
  • Idiomatic naming: snake_case members, PascalCase modules (configurable acronyms), UPPER_SNAKE constants, reserved-word mangling (next_next). Root namespace AWSCDK with AWS-prefix de-duplication (aws-s3AWSCDK::S3::Bucket).
  • Robust namespacing: pre-declares Ruby namespaces (module X; end) in dependency order to prevent NameError during nested type generation.
  • Security & type checking: injects Jsii::Type.check_type for runtime enforcement, and embeds type metadata as Base64-encoded JSON literals so no jsii-supplied identifier or docstring can inject into generated source via string interpolation.
  • Version management: shared version-utils.ts translates npm semver (incl. pre-release .alpha.1/.dev.1) to RubyGems ranges.

2. Ruby Runtime Core (@jsii/ruby-runtime)

  • Thread-safe IPC kernel: a re-entrant Monitor makes the bidirectional JSON-RPC stdio pipe safe under concurrency and re-entrant callbacks; an isolated stderr-draining thread prevents pipe deadlocks.
  • Callbacks & overrides: JS→Ruby callbacks (including super crossing back to the JS implementation), with user subclasses detected at construction time.
  • Object registry & lifecycle: proxies created via .allocate, mapped by $jsii.byref; deliberately never issues a guest-initiated del (matching the Python/Go runtimes) — cleanup is wholesale on kernel shutdown, which suits short-lived synth workloads.
  • Type-driven serialization: a dedicated Jsii::Serializer (rather than monkey-patching core types) handles by-value structs vs by-reference objects, avoiding leakage of host application state.

3. Compliance & Testing (@jsii/ruby-runtime-test)

  • Full compliance suite covering all 123 canonical compliance cases (diamond inheritance, async/sync overrides, property overrides, hash coercion, unions, nullability, collections, naming).
  • Ruby-specific unit specs: serializer round-trips, kernel concurrency, lazy autoload, and rbs validate. No skipped/pending tests.

4. CI / Build

  • GitHub Actions: Ruby 3.3 set up on every matrix leg, plus dedicated Ruby 3.4 and 4.0 legs; buildspec.yaml CodeBuild runtime added.
  • Wired into the compliance report (tools/jsii-compliance).

Compatibility & Known Limitations

  • Ruby 3.3+ on MRI (CRuby) only — gemspecs enforce required_ruby_version >= 3.3.0 (3.1/3.2 are EOL). JRuby/TruffleRuby are out of scope for the initial release.
  • Ambiguous unions: a Hash passed where a union admits more than one struct type is forwarded as-is; coercion only happens when exactly one struct arm makes it unambiguous.
  • Hash keys must be snake_case when supplied for struct coercion.
  • Async static methods go through the synchronous kernel path (the kernel's begin API only accepts object references — a limitation shared by all bindings).

Notes & Next Steps

  • Rosetta: the jsii-rosetta translation backend for Ruby is not included here and will follow in a subsequent PR.
  • Test fixtures: the committed test assemblies currently follow main and do not yet carry targets.ruby (it's compiler-generated). They — and the target-ruby snapshot — will be regenerated against the released Ruby-capable compiler before merge. See the warning above.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.


How to review this PR

The branch history is organized into seven self-contained review units, ordered so the
cross-cutting surface comes first and the large pure-addition trees are isolated. Total
shared-file footprint outside new ruby-* trees: 8 files
(spec configuration,
version-utils + its test, pacmak target registration, kernel.test.ts naming expectations,
compliance suite/report, CI matrix, tsconfig-base, one build-tools glob).

# Commit Scope Size
1 feat(spec) targets.ruby schema — the only assembly-facing change 1 file, +16
2 feat(pacmak) Ruby version-range support in shared version-utils, with tests 2 files, +199
3 feat(ruby) @jsii/ruby-runtime — the runtime gem (pure addition) 26 files, +2,510
4 feat(pacmak) the Ruby code-generation target (new file + 2-line registration; the bulk is the generated-code snapshot) 6 files, +26,593
5 test(ruby) runtime test suite + fixture targets.ruby declarations 42 files, +3,916
6 test(compliance) compliance-matrix wiring; absent report files render n/a (not a false 0.00%) 3 files, ±144/127
7 ci build-matrix rows + ignoreDeprecations (called out for review in the commit message) 4 files, +81

Suggested reading order: 1 → 2 → 7 → 6 (the entire cross-cutting surface, ~15 minutes) —
then 3 (the runtime) and 5 (its tests) as one unit, and 4 last (of its +26.6k, ~24k is the
committed generated-code snapshot; the generator itself is lib/targets/ruby.ts, ~2.4k lines).

Checks on this PR stay red until aws/jsii-compiler#2663
merges and releases (the branch builds against that schema); green full-matrix runs exist on
the branch and the daily preview pipeline smoke-tests the published output — sequencing in
the RFC's Upstreaming sequence section (aws/aws-cdk-rfcs#939).

Additive schema only: `ruby { gem, module, acronyms? }` joins the existing
per-language target blocks. The `.jsii` assembly format is unchanged —
AssemblyTargets already carries an index signature, so existing assemblies
and every other language are unaffected.
toRubyVersionRange converts semver ranges to RubyGems requirement syntax
(throwing on `||` OR-ranges, which Gem::Requirement cannot express), and
toReleaseVersion gains a RUBY case with a strict prerelease-label grammar.
Shared file, self-contained change, fully covered in version-utils.test.ts
including the error paths.
Pure addition. The Ruby guest runtime: a Monitor-serialized kernel driving
the standard Node sidecar over line-delimited JSON (all twelve wire verbs),
a type-driven serializer for every value envelope, override discovery from
generated metadata tables, hash-to-struct and Proc-to-interface (SAM)
coercion, lazy type registration/hydration, and an error taxonomy that
separates kernel faults from remote JS exceptions. Hand-written RBS
signatures under sig/. No dependencies beyond stdlib + base64 (a Ruby
3.4+ default-gem unbundling requirement).
@omarqureshi
omarqureshi force-pushed the ruby-language-bindings branch 2 times, most recently from 0dc40de to f4a25f4 Compare July 23, 2026 21:01
The generator (lib/targets/ruby.ts, new file): classes with flattened
member re-emission so `super` works in guest overrides, interfaces as
modules, structs with keyword constructors, enums, per-assembly acronym
config read from targets.ruby (no built-in lists), lazy loading via paired
autoload registration, RBS signature emission, YARD docs with
rosetta-translated examples, reserved-word escaping synced with the
runtime, and injection-safe string escaping at every interpolation site.

Cross-cutting surface is two lines in lib/targets/index.ts (target
registration) and a one-line generalization of package-ruby.bin's gemspec
glob. Coverage: the target-ruby snapshot corpus over jsii-calc plus
focused naming/casing tests.
packages/@jsii/ruby-runtime-test (new): 14 compliance spec files tagged
case-for-case against tools/jsii-compliance/suite.ts, plus unit specs for
the riskiest machinery (autoload, callbacks, serializer, SAM coercion,
concurrency, RBS validation with a negative control). generate.sh
regenerates the jsii-calc bindings unconditionally so specs can never run
against stale generated code.

Enablement edits outside the new package: targets.ruby declarations on the
four jsii-calc fixture assemblies, matching ruby naming expectations in
@jsii/kernel's kernel.test.ts, and a bash shebang on update-all.sh.
…nestly

suite.ts registers the ruby binding's report path. report.ts previously
iterated the report files present on disk; it now iterates the declared
bindings, which surfaces a latent presentation bug: a language whose
report file is absent in a given checkout would render as a false 0.00%.
Absent reports now render as "n/a" cells with an (n/a) header — languages
with report files are computed exactly as before, and the regenerated
gh-pages report reflects both changes.
Ruby 3.3 joins every job's setup and the OS matrix gains Ruby 3.4 and 4.0
rows (Linux/macOS/Windows). yarn.lock carries the ruby-runtime test
package's dependency graph.

tsconfig-base.json gains ignoreDeprecations "6.0": the shared compiler
options include settings TypeScript 6 flags as deprecated, and the Ruby
target was developed against a TS 6 toolchain. This is repo-wide and
deliberately surfaced here for review — it can be dropped if the
mainline toolchain does not need it.
@omarqureshi
omarqureshi force-pushed the ruby-language-bindings branch from f4a25f4 to 0514baf Compare July 24, 2026 08:59
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