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
149 changes: 125 additions & 24 deletions ext/ruby_wasm/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::path::PathBuf;
use std::{collections::HashMap, path::PathBuf};

use magnus::{
eval, exception, function, method,
Expand Down Expand Up @@ -51,16 +51,20 @@ impl WasiVfs {
}

fn map_dir(&self, guest_dir: String, host_dir: String) {
self.0.borrow_mut().map_dirs.push((guest_dir.into(), host_dir.into()));
self.0
.borrow_mut()
.map_dirs
.push((guest_dir.into(), host_dir.into()));
}

fn pack(&self, wasm_bytes: bytes::Bytes) -> Result<bytes::Bytes, Error> {
let output_bytes = wasi_vfs_cli::pack(&wasm_bytes, self.0.borrow().map_dirs.clone()).map_err(|e| {
Error::new(
exception::standard_error(),
format!("failed to pack wasi vfs: {}", e),
)
})?;
let output_bytes = wasi_vfs_cli::pack(&wasm_bytes, self.0.borrow().map_dirs.clone())
.map_err(|e| {
Error::new(
exception::standard_error(),
format!("failed to pack wasi vfs: {}", e),
)
})?;
Ok(output_bytes.into())
}
}
Expand All @@ -70,9 +74,14 @@ struct ComponentLink(std::cell::RefCell<Option<wit_component::Linker>>);

impl ComponentLink {
fn new() -> Self {
Self(std::cell::RefCell::new(Some(wit_component::Linker::default())))
Self(std::cell::RefCell::new(Some(
wit_component::Linker::default(),
)))
}
fn linker(&self, body: impl FnOnce(wit_component::Linker) -> Result<wit_component::Linker, Error>) -> Result<(), Error> {
fn linker(
&self,
body: impl FnOnce(wit_component::Linker) -> Result<wit_component::Linker, Error>,
) -> Result<(), Error> {
let mut linker = self.0.take().ok_or_else(|| {
Error::new(
exception::standard_error(),
Expand Down Expand Up @@ -105,24 +114,16 @@ impl ComponentLink {
})
}
fn validate(&self, validate: bool) -> Result<(), Error> {
self.linker(|linker| {
Ok(linker.validate(validate))
})
self.linker(|linker| Ok(linker.validate(validate)))
}
fn stack_size(&self, size: u32) -> Result<(), Error> {
self.linker(|linker| {
Ok(linker.stack_size(size))
})
self.linker(|linker| Ok(linker.stack_size(size)))
}
fn stub_missing_functions(&self, stub: bool) -> Result<(), Error> {
self.linker(|linker| {
Ok(linker.stub_missing_functions(stub))
})
self.linker(|linker| Ok(linker.stub_missing_functions(stub)))
}
fn use_built_in_libdl(&self, use_libdl: bool) -> Result<(), Error> {
self.linker(|linker| {
Ok(linker.use_built_in_libdl(use_libdl))
})
self.linker(|linker| Ok(linker.use_built_in_libdl(use_libdl)))
}
fn encode(&self) -> Result<bytes::Bytes, Error> {
// Take the linker out of the cell and consume it
Expand All @@ -142,6 +143,85 @@ impl ComponentLink {
}
}

#[wrap(class = "RubyWasmExt::ComponentEncode")]
struct ComponentEncode(std::cell::RefCell<Option<wit_component::ComponentEncoder>>);

impl ComponentEncode {
fn new() -> Self {
Self(std::cell::RefCell::new(Some(
wit_component::ComponentEncoder::default(),
)))
}

fn encoder(
&self,
body: impl FnOnce(
wit_component::ComponentEncoder,
) -> Result<wit_component::ComponentEncoder, Error>,
) -> Result<(), Error> {
let mut encoder = self.0.take().ok_or_else(|| {
Error::new(
exception::standard_error(),
"encoder is already consumed".to_string(),
)
})?;
encoder = body(encoder)?;
self.0.replace(Some(encoder));
Ok(())
}

fn validate(&self, validate: bool) -> Result<(), Error> {
self.encoder(|encoder| Ok(encoder.validate(validate)))
}

fn adapter(&self, name: String, module: bytes::Bytes) -> Result<(), Error> {
self.encoder(|encoder| {
encoder.adapter(&name, &module).map_err(|e| {
Error::new(
exception::standard_error(),
format!("failed to encode adapter: {}", e),
)
})
})
}

fn module(&self, module: bytes::Bytes) -> Result<(), Error> {
self.encoder(|encoder| {
encoder.module(&module).map_err(|e| {
Error::new(
exception::standard_error(),
format!("failed to encode module: {}", e),
)
})
})
}

fn realloc_via_memory_grow(&self, realloc: bool) -> Result<(), Error> {
self.encoder(|encoder| Ok(encoder.realloc_via_memory_grow(realloc)))
}

fn import_name_map(&self, map: HashMap<String, String>) -> Result<(), Error> {
self.encoder(|encoder| Ok(encoder.import_name_map(map)))
}

fn encode(&self) -> Result<bytes::Bytes, Error> {
// Take the encoder out of the cell and consume it
let encoder = self.0.borrow_mut().take().ok_or_else(|| {
Error::new(
exception::standard_error(),
"encoder is already consumed".to_string(),
)
})?;
let encoded = encoder.encode().map_err(|e| {
Error::new(
exception::standard_error(),
format!("failed to encode component: {}", e),
)
})?;
Ok(encoded.into())
}
}

#[magnus::init]
fn init(ruby: &Ruby) -> Result<(), Error> {
let module = RUBY_WASM.get_inner_with(ruby);
Expand All @@ -161,9 +241,30 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
component_link.define_method("adapter", method!(ComponentLink::adapter, 2))?;
component_link.define_method("validate", method!(ComponentLink::validate, 1))?;
component_link.define_method("stack_size", method!(ComponentLink::stack_size, 1))?;
component_link.define_method("stub_missing_functions", method!(ComponentLink::stub_missing_functions, 1))?;
component_link.define_method("use_built_in_libdl", method!(ComponentLink::use_built_in_libdl, 1))?;
component_link.define_method(
"stub_missing_functions",
method!(ComponentLink::stub_missing_functions, 1),
)?;
component_link.define_method(
"use_built_in_libdl",
method!(ComponentLink::use_built_in_libdl, 1),
)?;
component_link.define_method("encode", method!(ComponentLink::encode, 0))?;

let component_encode = module.define_class("ComponentEncode", ruby.class_object())?;
component_encode.define_singleton_method("new", function!(ComponentEncode::new, 0))?;
component_encode.define_method("validate", method!(ComponentEncode::validate, 1))?;
component_encode.define_method("adapter", method!(ComponentEncode::adapter, 2))?;
component_encode.define_method("module", method!(ComponentEncode::module, 1))?;
component_encode.define_method(
"realloc_via_memory_grow",
method!(ComponentEncode::realloc_via_memory_grow, 1),
)?;
component_encode.define_method(
"import_name_map",
method!(ComponentEncode::import_name_map, 1),
)?;
component_encode.define_method("encode", method!(ComponentEncode::encode, 0))?;

Ok(())
}
1 change: 1 addition & 0 deletions lib/ruby_wasm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require_relative "ruby_wasm/version"
require_relative "ruby_wasm/util"
require_relative "ruby_wasm/build"
require_relative "ruby_wasm/feature_set"
require_relative "ruby_wasm/packager"
require_relative "ruby_wasm/packager/component_adapter"
require_relative "ruby_wasm/packager/file_system"
Expand Down
16 changes: 11 additions & 5 deletions lib/ruby_wasm/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,12 @@ def pack(args)
private

def build_config(options)
build_source, all_default_exts = compute_build_source(options)
# @type var config: Packager::build_config
config = { target: options[:target_triplet], src: compute_build_source(options) }
config = { target: options[:target_triplet], src: build_source }
case options[:profile]
when "full"
config[:default_exts] = config[:src][:all_default_exts]
config[:default_exts] = all_default_exts || ""
env_additional_exts = ENV["RUBY_WASM_ADDITIONAL_EXTS"] || ""
unless env_additional_exts.empty?
config[:default_exts] += "," + env_additional_exts
Expand Down Expand Up @@ -203,7 +204,7 @@ def compute_build_source(options)
local_source = { type: "local", path: src_name }
# @type var local_source: RubyWasm::Packager::build_source
local_source = local_source.merge(name: "local", patches: [])
return local_source
return [local_source, nil]
end
# Otherwise, it's an unknown source.
raise(
Expand All @@ -212,7 +213,9 @@ def compute_build_source(options)
end
# Apply user-specified patches in addition to bundled patches.
source[:patches].concat(options[:patches])
source
# @type var all_default_exts: String
__skip__ = all_default_exts = source[:all_default_exts]
[source, all_default_exts]
end

# Retrieves the alias definitions for the Ruby sources.
Expand Down Expand Up @@ -305,7 +308,10 @@ def derive_packager(options)
end
end
RubyWasm.logger.info "Using Gemfile: #{definition.gemfiles}" if definition
RubyWasm::Packager.new(root, build_config(options), definition)
RubyWasm::Packager.new(
root, build_config(options), definition,
features: RubyWasm::FeatureSet.derive_from_env
)
end

def do_print_ruby_cache_key(packager)
Expand Down
30 changes: 30 additions & 0 deletions lib/ruby_wasm/feature_set.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
##
# A set of feature flags that can be used to enable or disable experimental features.
class RubyWasm::FeatureSet
def initialize(features)
@features = features
end

# Maps the feature to the environment variable.
FEATURES = {
dynamic_linking: "RUBY_WASM_EXPERIMENTAL_DYNAMIC_LINKING",
component_model: "RUBY_WASM_EXPERIMENTAL_COMPONENT_MODEL",
}.freeze
private_constant :FEATURES

# Derives the feature set from the environment variables. A feature
# is enabled if the corresponding environment variable is set to "1",
# otherwise it is disabled.
def self.derive_from_env
values = FEATURES.transform_values { |key| ENV[key] == "1" }
new(values)
end

def support_dynamic_linking?
@features[:dynamic_linking]
end

def support_component_model?
@features[:component_model] || @features[:dynamic_linking]
end
end
11 changes: 6 additions & 5 deletions lib/ruby_wasm/packager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ class RubyWasm::Packager
# * build
# @param config [Hash] The build config used for building Ruby.
# @param definition [Bundler::Definition] The Bundler definition.
def initialize(root, config = nil, definition = nil)
# @param features [RubyWasm::FeatureSet] The features used for packaging.
def initialize(root, config = nil, definition = nil, features: RubyWasm::FeatureSet.derive_from_env)
@root = root
@definition = definition
@config = config
@features = features
end

# Packages the Ruby code into a Wasm binary. (including extensions)
Expand All @@ -33,7 +35,7 @@ def package(executor, dest_dir, options)
fs.remove_non_runtime_files(executor)
fs.remove_stdlib(executor) unless options[:stdlib]

if full_build_options[:target] == "wasm32-unknown-wasip1" && !support_dynamic_linking?
if full_build_options[:target] == "wasm32-unknown-wasip1" && !features.support_component_model?
# wasi-vfs supports only WASI target
wasi_vfs = RubyWasmExt::WasiVfs.new
wasi_vfs.map_dir("/bundle", fs.bundle_dir)
Expand Down Expand Up @@ -61,9 +63,8 @@ def specs
@specs
end

# Checks if dynamic linking is supported.
def support_dynamic_linking?
ENV["RUBY_WASM_EXPERIMENTAL_DYNAMIC_LINKING"] == "1"
def features
@features
end

ALL_DEFAULT_EXTS =
Expand Down
15 changes: 13 additions & 2 deletions lib/ruby_wasm/packager/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def build_strategy
@build_strategy ||=
begin
has_exts = @packager.specs.any? { |spec| spec.extensions.any? }
if @packager.support_dynamic_linking?
if @packager.features.support_dynamic_linking?
DynamicLinking.new(@packager)
else
StaticLinking.new(@packager)
Expand Down Expand Up @@ -299,7 +299,18 @@ def derive_build
def build_and_link_exts(executor)
build = derive_build
ruby_root = build.crossruby.dest_dir
File.binread(File.join(ruby_root, "usr", "local", "bin", "ruby"))
module_bytes = File.binread(File.join(ruby_root, "usr", "local", "bin", "ruby"))
return module_bytes unless @packager.features.support_component_model?

linker = RubyWasmExt::ComponentEncode.new
linker.validate(true)
linker.module(module_bytes)
linker.adapter(
"wasi_snapshot_preview1",
File.binread(RubyWasm::Packager::ComponentAdapter.wasi_snapshot_preview1("reactor"))
)

linker.encode()
end

def user_exts(build)
Expand Down
2 changes: 1 addition & 1 deletion lib/ruby_wasm/packager/file_system.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def remove_non_runtime_files(executor)
usr/local/include
]

patterns << "**/*.so" unless @packager.support_dynamic_linking?
patterns << "**/*.so" unless @packager.features.support_dynamic_linking?
patterns.each do |pattern|
Dir
.glob(File.join(@dest_dir, pattern))
Expand Down
2 changes: 1 addition & 1 deletion sig/ruby_wasm/cli.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ module RubyWasm
private

def build_config: (cli_options options) -> Packager::build_config
def compute_build_source: (cli_options options) -> Packager::build_source
def compute_build_source: (cli_options options) -> [Packager::build_source, String?]
def self.build_source_aliases: (string root) -> Hash[string, Packager::build_source]
def self.bundled_patches_path: () -> string
def root: () -> string
Expand Down
10 changes: 10 additions & 0 deletions sig/ruby_wasm/ext.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,14 @@ module RubyWasmExt
def use_built_in_libdl: (bool) -> void
def encode: () -> bytes
end

class ComponentEncode
def initialize: () -> void
def validate: (bool) -> void
def adapter: (String name, bytes module) -> void
def module: (bytes module) -> void
def realloc_via_memory_grow: (bool) -> void
def import_name_map: (Hash[String, String] map) -> void
def encode: () -> bytes
end
end
12 changes: 12 additions & 0 deletions sig/ruby_wasm/feature_set.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class RubyWasm::FeatureSet
@features: Hash[Symbol, bool]

def initialize: (Hash[Symbol, bool]) -> void

FEATURES: Hash[Symbol, String]

def self.derive_from_env: () -> RubyWasm::FeatureSet

def support_dynamic_linking?: () -> bool
def support_component_model?: () -> bool
end
Loading