From 31c6e4e9544b99e48afbb616160e5d2be27e322d Mon Sep 17 00:00:00 2001 From: Ian Ker-Seymer Date: Mon, 18 Nov 2024 22:52:20 -0500 Subject: [PATCH] Add truffleruby support (#441) * Add truffleruby support * Remove panicking version of `RbConfig::get` * Push commit to trigger CI * More permissive version slug * Fix various compilation issues * Fix delta track for subtraction in tracking allocator * Enable lazy linking for truffle on linux * Enable lazy linking for truffle on linux * Attempt to parse ruby version from string when MAJOR not avail * Try harder to find ruby version * Disable relro so lazy binding will work on linux * Disable ruby-head for now * Update readme --- .github/workflows/ci.yml | 9 + Rakefile | 5 + crates/rb-sys-build/src/bindings.rs | 28 +-- .../rb-sys-build/src/bindings/stable_api.rs | 5 +- crates/rb-sys-build/src/cc.rs | 23 +- crates/rb-sys-build/src/rb_config.rs | 127 ++++++++--- crates/rb-sys-env/src/ruby_version.rs | 35 ++- .../src/ruby_test_executor.rs | 2 +- crates/rb-sys/build/features.rs | 9 +- crates/rb-sys/build/main.rs | 64 ++++-- crates/rb-sys/build/stable_api_config.rs | 24 +- crates/rb-sys/build/version.rs | 17 +- crates/rb-sys/src/hidden.rs | 1 + crates/rb-sys/src/macros.rs | 1 + crates/rb-sys/src/ruby_abi_version.rs | 8 + crates/rb-sys/src/tracking_allocator.rs | 212 +++++++++++------- crates/rb-sys/src/utils.rs | 15 +- gem/lib/rb_sys/cargo_builder.rb | 5 + gem/lib/rb_sys/mkmf.rb | 2 +- readme.md | 7 +- 20 files changed, 417 insertions(+), 182 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d74e828d..6e1ee451 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,6 +44,15 @@ jobs: rust_toolchain: stable - os: windows-2022 rust_toolchain: stable + include: + - ruby_version: "truffleruby" + sys: + os: ubuntu-latest + rust_toolchain: stable + - ruby_version: "truffleruby" + sys: + os: macos-latest + rust_toolchain: stable exclude: # Missing symbols for some reason, need to fix - ruby_version: "2.6" diff --git a/Rakefile b/Rakefile index 0e544faf..d95a978a 100644 --- a/Rakefile +++ b/Rakefile @@ -15,6 +15,11 @@ def cargo_test_task(name, *args, crate: name) desc "Run cargo tests for #{name.inspect} against current Ruby" task task_name do + if RUBY_ENGINE != "ruby" + puts "Skipping #{task_name} because it only works with MRI Ruby" + next + end + default_args = ENV["CI"] || extra_args.include?("--verbose") ? [] : ["--quiet"] test_args = ENV["CI"] || extra_args.include?("--verbose") ? ["--", "--nocapture"] : [] sh "cargo", "test", *default_args, *extra_args, *args, "-p", crate, *test_args diff --git a/crates/rb-sys-build/src/bindings.rs b/crates/rb-sys-build/src/bindings.rs index 603e892a..59e785f4 100644 --- a/crates/rb-sys-build/src/bindings.rs +++ b/crates/rb-sys-build/src/bindings.rs @@ -5,6 +5,7 @@ use crate::cc::Build; use crate::utils::is_msvc; use crate::{debug_log, RbConfig}; use quote::ToTokens; +use stable_api::{categorize_bindings, opaqueify_bindings}; use std::fs::File; use std::io::Write; use std::path::{Path, PathBuf}; @@ -21,10 +22,13 @@ pub fn generate( ) -> Result> { let out_dir = PathBuf::from(env::var("OUT_DIR")?); - let mut clang_args = vec![ - format!("-I{}", rbconfig.get("rubyhdrdir")), - format!("-I{}", rbconfig.get("rubyarchhdrdir")), - ]; + let mut clang_args = vec![]; + if let Some(ruby_include_dir) = rbconfig.get("rubyhdrdir") { + clang_args.push(format!("-I{}", ruby_include_dir)); + } + if let Some(ruby_arch_include_dir) = rbconfig.get("rubyarchhdrdir") { + clang_args.push(format!("-I{}", ruby_arch_include_dir)); + } clang_args.extend(Build::default_cflags()); clang_args.extend(rbconfig.cflags.clone()); @@ -47,8 +51,10 @@ pub fn generate( let bindings = default_bindgen(clang_args) .allowlist_file(".*ruby.*") .blocklist_item("ruby_abi_version") + .blocklist_function("rb_tr_abi_version") .blocklist_function("^__.*") .blocklist_item("RData") + .blocklist_function("rb_tr_rdata") .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())); let bindings = if cfg!(feature = "bindgen-rbimpls") { @@ -67,7 +73,7 @@ pub fn generate( .blocklist_item("^_bindgen_ty_9.*") }; - let bindings = stable_api::opaqueify_bindings(rbconfig, bindings, &mut wrapper_h); + let bindings = opaqueify_bindings(rbconfig, bindings, &mut wrapper_h); let mut tokens = { write!(std::io::stderr(), "{}", wrapper_h)?; @@ -76,13 +82,9 @@ pub fn generate( syn::parse_file(&code_string)? }; - let ruby_version = rbconfig.ruby_program_version(); - let ruby_platform = rbconfig.platform(); + let slug = rbconfig.ruby_version_slug(); let crate_version = env!("CARGO_PKG_VERSION"); - let out_path = out_dir.join(format!( - "bindings-{}-{}-{}.rs", - crate_version, ruby_platform, ruby_version - )); + let out_path = out_dir.join(format!("bindings-{}-{}.rs", crate_version, slug)); let code = { sanitizer::ensure_backwards_compatible_encoding_pointers(&mut tokens); @@ -93,7 +95,7 @@ pub fn generate( } push_cargo_cfg_from_bindings(&tokens, cfg_out)?; - stable_api::categorize_bindings(&mut tokens); + categorize_bindings(&mut tokens); tokens.into_token_stream().to_string() }; @@ -121,7 +123,7 @@ fn clean_docs(rbconfig: &RbConfig, syntax: &mut syn::File) { return; } - let ver = rbconfig.ruby_program_version(); + let ver = rbconfig.ruby_version_slug(); sanitizer::cleanup_docs(syntax, &ver).unwrap_or_else(|e| { debug_log!("WARN: failed to clean up docs, skipping: {}", e); diff --git a/crates/rb-sys-build/src/bindings/stable_api.rs b/crates/rb-sys-build/src/bindings/stable_api.rs index 56d2e948..479da284 100644 --- a/crates/rb-sys-build/src/bindings/stable_api.rs +++ b/crates/rb-sys-build/src/bindings/stable_api.rs @@ -191,7 +191,10 @@ fn gen_opaque_struct( .allowlist_type(struct_name) } -fn get_version_specific_opaque_structs(major_minor: (u32, u32)) -> Vec<&'static str> { +fn get_version_specific_opaque_structs(major_minor: Option<(u32, u32)>) -> Vec<&'static str> { + let Some(major_minor) = major_minor else { + return vec![]; + }; let mut result = vec![]; let (major, minor) = major_minor; diff --git a/crates/rb-sys-build/src/cc.rs b/crates/rb-sys-build/src/cc.rs index d4f79dd2..52590456 100644 --- a/crates/rb-sys-build/src/cc.rs +++ b/crates/rb-sys-build/src/cc.rs @@ -170,12 +170,21 @@ impl Build { } fn get_include_args(rb: &rb_config::RbConfig) -> Vec { - vec![ - format!("-I{}", rb.get("rubyhdrdir")), - format!("-I{}", rb.get("rubyarchhdrdir")), - format!("-I{}/include/internal", rb.get("rubyhdrdir")), - format!("-I{}/include/impl", rb.get("rubyhdrdir")), - ] + let mut args = vec![]; + if let Some(include_dir) = rb.get("rubyhdrdir") { + args.push(format!("-I{}", include_dir)); + } + if let Some(arch_include_dir) = rb.get("rubyarchhdrdir") { + args.push(format!("-I{}", arch_include_dir)); + } + if let Some(internal_include_dir) = rb.get("rubyhdrdir") { + args.push(format!("-I{}/include/internal", internal_include_dir)); + } + if let Some(impl_include_dir) = rb.get("rubyhdrdir") { + args.push(format!("-I{}/include/impl", impl_include_dir)); + } + + args } fn get_common_args() -> Vec { @@ -292,7 +301,7 @@ fn get_tool_from_rb_config_or_env(env_var: &str) -> Option { get_tool_from_env(env_var) .filter(|s| !s.is_empty()) - .or_else(|| rb.get_optional(env_var)) + .or_else(|| rb.get(env_var)) } fn get_tool_from_env(env_var: &str) -> Option { diff --git a/crates/rb-sys-build/src/rb_config.rs b/crates/rb-sys-build/src/rb_config.rs index 9edade70..e80658d2 100644 --- a/crates/rb-sys-build/src/rb_config.rs +++ b/crates/rb-sys-build/src/rb_config.rs @@ -108,9 +108,12 @@ impl RbConfig { /// Pushes the `LIBRUBYARG` flags so Ruby will be linked. pub fn link_ruby(&mut self, is_static: bool) -> &mut Self { - let libdir = self.get("libdir"); + let Some(libdir) = self.get("libdir") else { + return self; + }; + self.push_search_path(libdir.as_str()); - self.push_dldflags(&format!("-L{}", &self.get("libdir"))); + self.push_dldflags(&format!("-L{}", libdir)); let librubyarg = if is_static { self.get("LIBRUBYARG_STATIC") @@ -118,6 +121,14 @@ impl RbConfig { self.get("LIBRUBYARG_SHARED") }; + let librubyarg = match librubyarg { + Some(lib) => lib, + None => { + debug_log!("WARN: LIBRUBYARG not found in RbConfig, skipping linking Ruby"); + return self; + } + }; + if is_msvc() { for lib in librubyarg.split_whitespace() { self.push_library(lib); @@ -125,11 +136,11 @@ impl RbConfig { let mut to_link: Vec = vec![]; - if let Some(libs) = self.get_optional("LIBS") { + if let Some(libs) = self.get("LIBS") { to_link.extend(libs.split_whitespace().map(|s| s.to_string())); } - if let Some(libs) = self.get_optional("LOCAL_LIBS") { + if let Some(libs) = self.get("LOCAL_LIBS") { to_link.extend(libs.split_whitespace().map(|s| s.to_string())); } @@ -149,8 +160,11 @@ impl RbConfig { /// Get the name for libruby-static (i.e. `ruby.3.1-static`). pub fn libruby_static_name(&self) -> String { - self.get("LIBRUBY_A") - .trim_start_matches("lib") + let Some(lib) = self.get("LIBRUBY_A") else { + return format!("{}-static", self.libruby_so_name()); + }; + + lib.trim_start_matches("lib") .trim_end_matches(".a") .to_string() } @@ -158,12 +172,13 @@ impl RbConfig { /// Get the name for libruby (i.e. `ruby.3.1`) pub fn libruby_so_name(&self) -> String { self.get("RUBY_SO_NAME") + .unwrap_or_else(|| "ruby".to_string()) } /// Get the platform for the current ruby. pub fn platform(&self) -> String { - self.get_optional("platform") - .unwrap_or_else(|| self.get("arch")) + self.get("platform") + .unwrap_or_else(|| self.get("arch").expect("arch not found")) } /// Filter the libs, removing the ones that are not needed. @@ -179,22 +194,28 @@ impl RbConfig { } /// Returns the current ruby program version. - pub fn ruby_program_version(&self) -> String { - if let Some(progv) = self.get_optional("RUBY_PROGRAM_VERSION") { + pub fn ruby_version_slug(&self) -> String { + let ver = if let Some(progv) = self.get("RUBY_PROGRAM_VERSION") { progv - } else { + } else if let Some(major_minor) = self.major_minor() { format!( "{}.{}.{}", - self.get("MAJOR"), - self.get("MINOR"), - self.get("TEENY") + major_minor.0, + major_minor.1, + self.get("TEENY").unwrap_or_else(|| "0".to_string()) ) - } + } else if let Some(fallback) = self.get("ruby_version") { + fallback + } else { + panic!("RUBY_PROGRAM_VERSION not found") + }; + + format!("{}-{}-{}", self.ruby_engine(), self.platform(), ver) } /// Get the CPPFLAGS from the RbConfig, making sure to subsitute variables. pub fn cppflags(&self) -> Vec { - if let Some(cppflags) = self.get_optional("CPPFLAGS") { + if let Some(cppflags) = self.get("CPPFLAGS") { let flags = self.subst_shell_variables(&cppflags); shellsplit(flags) } else { @@ -202,16 +223,9 @@ impl RbConfig { } } - /// Returns the value of the given key from the either the matching - /// `RBCONFIG_{key}` environment variable or `RbConfig::CONFIG[{key}]` hash. - pub fn get(&self, key: &str) -> String { - self.get_optional(key) - .unwrap_or_else(|| panic!("Key not found: {}", key)) - } - /// Returns true if the current Ruby is cross compiling. pub fn is_cross_compiling(&self) -> bool { - if let Some(cross) = self.get_optional("CROSS_COMPILING") { + if let Some(cross) = self.get("CROSS_COMPILING") { cross == "yes" || cross == "1" } else { false @@ -220,7 +234,7 @@ impl RbConfig { /// Returns the value of the given key from the either the matching /// `RBCONFIG_{key}` environment variable or `RbConfig::CONFIG[{key}]` hash. - pub fn get_optional(&self, key: &str) -> Option { + pub fn get(&self, key: &str) -> Option { self.try_rbconfig_env(key) .or_else(|| self.try_value_map(key)) } @@ -243,10 +257,10 @@ impl RbConfig { } /// Get major/minor version tuple of Ruby - pub fn major_minor(&self) -> (u32, u32) { - let major = self.get("MAJOR").parse::().unwrap(); - let minor = self.get("MINOR").parse::().unwrap(); - (major, minor) + pub fn major_minor(&self) -> Option<(u32, u32)> { + let major = self.get("MAJOR").map(|v| v.parse::())?.ok()?; + let minor = self.get("MINOR").map(|v| v.parse::())?.ok()?; + Some((major, minor)) } /// Get the rb_config output for cargo @@ -341,15 +355,34 @@ impl RbConfig { // Check if has ABI version pub fn has_ruby_dln_check_abi(&self) -> bool { - let major = self.get("MAJOR").parse::().unwrap(); - let minor = self.get("MINOR").parse::().unwrap(); - let patchlevel = self.get("PATCHLEVEL").parse::().unwrap(); + let Some((major, minor)) = self.major_minor() else { + return false; + }; + + let patchlevel = self + .get("PATCHLEVEL") + .and_then(|v| v.parse::().ok()) + .unwrap_or(-1); // Ruby has ABI version on verion 3.2 and later only on development // versions major >= 3 && minor >= 2 && patchlevel == -1 && !cfg!(target_family = "windows") } + /// The RUBY_ENGINE we are building for + pub fn ruby_engine(&self) -> RubyEngine { + if let Some(engine) = self.get("ruby_install_name") { + match engine.as_str() { + "ruby" => RubyEngine::Mri, + "jruby" => RubyEngine::JRuby, + "truffleruby" => RubyEngine::TruffleRuby, + _ => RubyEngine::Mri, // not sure how stable this is, so default to MRI to avoid breaking things + } + } else { + RubyEngine::Mri + } + } + // Examines the string from shell variables and expands them with values in the value_map fn subst_shell_variables(&self, input: &str) -> String { let mut result = String::new(); @@ -371,7 +404,7 @@ impl RbConfig { let key = &input[start..end]; - if let Some(val) = self.get_optional(key) { + if let Some(val) = self.get(key) { result.push_str(&val); } else if let Some(val) = env::var_os(key) { result.push_str(&val.to_string_lossy()); @@ -392,8 +425,12 @@ impl RbConfig { } pub fn have_ruby_header>(&self, header: T) -> bool { - let ruby_include_dir: PathBuf = self.get("rubyhdrdir").into(); - ruby_include_dir.join(header.as_ref()).exists() + let Some(ruby_include_dir) = self.get("rubyhdrdir") else { + return false; + }; + PathBuf::from(ruby_include_dir) + .join(header.as_ref()) + .exists() } fn push_search_path>(&mut self, path: T) -> &mut Self { @@ -439,6 +476,23 @@ impl RbConfig { } } +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum RubyEngine { + Mri, + TruffleRuby, + JRuby, +} + +impl std::fmt::Display for RubyEngine { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RubyEngine::Mri => write!(f, "mri"), + RubyEngine::TruffleRuby => write!(f, "truffleruby"), + RubyEngine::JRuby => write!(f, "jruby"), + } + } +} + fn capture_name(regex: &Regex, arg: &str) -> Option { regex .captures(arg) @@ -753,8 +807,7 @@ mod tests { env::set_var("RBCONFIG_libdir", "/foo"); let rb_config = RbConfig::new(); - assert_eq!(rb_config.get("libdir"), "/foo"); - assert_eq!(rb_config.get_optional("libdir"), Some("/foo".into())); + assert_eq!(rb_config.get("libdir"), Some("/foo".into())); env::remove_var("RBCONFIG_libdir"); }); diff --git a/crates/rb-sys-env/src/ruby_version.rs b/crates/rb-sys-env/src/ruby_version.rs index e1758f1e..8cc6a50c 100644 --- a/crates/rb-sys-env/src/ruby_version.rs +++ b/crates/rb-sys-env/src/ruby_version.rs @@ -104,16 +104,35 @@ impl From<(u8, u8, u8)> for RubyVersion { } } -fn verpart(env: &HashMap, key: &str) -> u8 { - env.get(key).unwrap().parse().unwrap() -} - impl RubyVersion { pub(crate) fn from_raw_environment(env: &HashMap) -> Self { - Self { - major: verpart(env, "MAJOR"), - minor: verpart(env, "MINOR"), - teeny: verpart(env, "TEENY"), + match (env.get("MAJOR"), env.get("MINOR"), env.get("TEENY")) { + (Some(major), Some(minor), Some(teeny)) => { + let major = major.parse().expect("MAJOR is not a number"); + let minor = minor.parse().expect("MINOR is not a number"); + let teeny = teeny.parse().expect("TEENY is not a number"); + + Self { + major, + minor, + teeny, + } + } + _ => { + let env_ruby_version = env.get("ruby_version").cloned().unwrap_or_else(|| { + std::env::var("RUBY_VERSION").expect("RUBY_VERSION is not set") + }); + + let mut ruby_version = env_ruby_version + .split('.') + .map(|s| s.parse().expect("version component is not a number")); + + Self { + major: ruby_version.next().expect("major"), + minor: ruby_version.next().expect("minor"), + teeny: ruby_version.next().expect("teeny"), + } + } } } } diff --git a/crates/rb-sys-test-helpers/src/ruby_test_executor.rs b/crates/rb-sys-test-helpers/src/ruby_test_executor.rs index 1fd9fb32..da9b5c08 100644 --- a/crates/rb-sys-test-helpers/src/ruby_test_executor.rs +++ b/crates/rb-sys-test-helpers/src/ruby_test_executor.rs @@ -150,7 +150,7 @@ pub unsafe fn setup_ruby_unguarded() { let mut argv: [*mut i8; 3] = [ "ruby\0".as_ptr() as _, "-e\0".as_ptr() as _, - "\0".as_ptr() as _, + "nil\0".as_ptr() as _, ]; ruby_process_options(argv.len() as _, argv.as_mut_ptr() as _) as _ diff --git a/crates/rb-sys/build/features.rs b/crates/rb-sys/build/features.rs index 49e646e7..3361e94c 100644 --- a/crates/rb-sys/build/features.rs +++ b/crates/rb-sys/build/features.rs @@ -3,7 +3,9 @@ use rb_sys_build::{utils::is_mswin_or_mingw, RbConfig}; use crate::version::Version; pub(crate) fn is_global_allocator_enabled(rb_config: &RbConfig) -> bool { - let (major, minor) = rb_config.major_minor(); + let Some((major, minor)) = rb_config.major_minor() else { + return false; + }; let current_version = Version::new(major, minor); let two_four = Version::new(2, 4); let is_enabled = is_env_variable_defined("CARGO_FEATURE_GLOBAL_ALLOCATOR"); @@ -43,7 +45,10 @@ pub(crate) fn is_ruby_static_enabled(rbconfig: &RbConfig) -> bool { Ok(val) => val == "true" || val == "1", _ => { is_env_variable_defined("CARGO_FEATURE_RUBY_STATIC") - || rbconfig.get("ENABLE_SHARED") == "no" + || rbconfig + .get("ENABLE_SHARED") + .map(|v| v == "no") + .unwrap_or(false) } } } diff --git a/crates/rb-sys/build/main.rs b/crates/rb-sys/build/main.rs index 1d0a21cd..6daabacd 100644 --- a/crates/rb-sys/build/main.rs +++ b/crates/rb-sys/build/main.rs @@ -4,7 +4,7 @@ mod stable_api_config; mod version; use features::*; -use rb_sys_build::{bindings, RbConfig}; +use rb_sys_build::{bindings, RbConfig, RubyEngine}; use std::io::Write; use std::{ env, @@ -30,14 +30,8 @@ fn main() { let mut rbconfig = RbConfig::current(); let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); - let ruby_version = rbconfig.ruby_program_version(); - let ruby_platform = rbconfig.platform(); - let crate_version = env!("CARGO_PKG_VERSION"); - let cfg_capture_path = out_dir.join(format!( - "cfg-capture-{}-{}-{}", - crate_version, ruby_platform, ruby_version - )); - let mut cfg_capture_file = File::create(cfg_capture_path).unwrap(); + let cfg_capture_path = out_dir.join(format!("cfg-capture-{}", rbconfig.ruby_version_slug())); + let mut cfg_capture_file = File::create(cfg_capture_path).expect("create cfg capture file"); println!("cargo:rerun-if-env-changed=RUBY_ROOT"); println!("cargo:rerun-if-env-changed=RUBY_VERSION"); @@ -61,7 +55,7 @@ fn main() { export_cargo_cfg(&mut rbconfig, &mut cfg_capture_file); #[cfg(feature = "stable-api")] - stable_api_config::setup(Version::current(&rbconfig)).expect("could not setup stable API"); + stable_api_config::setup(&rbconfig).expect("could not setup stable API"); if is_link_ruby_enabled() { link_libruby(&mut rbconfig); @@ -86,6 +80,14 @@ macro_rules! cfg_capture { }; } +macro_rules! cfg_capture_opt { + ($file:expr, $fmt:expr, $opt:expr) => { + if let Some(val) = $opt { + cfg_capture!($file, $fmt, val); + } + }; +} + fn add_unsupported_link_args_to_blocklist(rbconfig: &mut RbConfig) { rbconfig.blocklist_link_arg("-Wl,--compress-debug-sections=zlib"); rbconfig.blocklist_link_arg("-s"); @@ -132,6 +134,17 @@ fn export_cargo_cfg(rbconfig: &mut RbConfig, cap: &mut File) { println!("cargo:rustc-cfg=has_ruby_abi_version"); } + println!("cargo:rustc-check-cfg=cfg(ruby_engine, values(\"mri\", \"truffleruby\"))"); + match rbconfig.ruby_engine() { + RubyEngine::Mri => { + println!("cargo:rustc-cfg=ruby_engine=\"mri\""); + } + RubyEngine::TruffleRuby => { + println!("cargo:rustc-cfg=ruby_engine=\"truffleruby\""); + } + _ => panic!("unsupported ruby engine"), + } + let version = Version::current(rbconfig); for v in SUPPORTED_RUBY_VERSIONS.iter() { @@ -208,17 +221,24 @@ fn export_cargo_cfg(rbconfig: &mut RbConfig, cap: &mut File) { } } - cfg_capture!(cap, "cargo:root={}", rbconfig.get("prefix")); - cfg_capture!(cap, "cargo:include={}", rbconfig.get("includedir")); - cfg_capture!(cap, "cargo:archinclude={}", rbconfig.get("archincludedir")); - cfg_capture!(cap, "cargo:version={}", rbconfig.get("ruby_version")); - cfg_capture!(cap, "cargo:major={}", rbconfig.get("MAJOR")); - cfg_capture!(cap, "cargo:minor={}", rbconfig.get("MINOR")); - cfg_capture!(cap, "cargo:teeny={}", rbconfig.get("TEENY")); - cfg_capture!(cap, "cargo:patchlevel={}", rbconfig.get("PATCHLEVEL")); + cfg_capture_opt!(cap, "cargo:root={}", rbconfig.get("prefix")); + cfg_capture_opt!(cap, "cargo:include={}", rbconfig.get("includedir")); + cfg_capture_opt!(cap, "cargo:archinclude={}", rbconfig.get("archincludedir")); + cfg_capture_opt!(cap, "cargo:libdir={}", rbconfig.get("libdir")); + cfg_capture_opt!(cap, "cargo:version={}", rbconfig.get("ruby_version")); + cfg_capture_opt!(cap, "cargo:major={}", rbconfig.get("MAJOR")); + cfg_capture_opt!(cap, "cargo:minor={}", rbconfig.get("MINOR")); + cfg_capture_opt!(cap, "cargo:teeny={}", rbconfig.get("TEENY")); + cfg_capture_opt!(cap, "cargo:patchlevel={}", rbconfig.get("PATCHLEVEL")); + cfg_capture!(cap, "cargo:engine={}", rbconfig.ruby_engine()); for key in rbconfig.all_keys() { - cfg_capture!(cap, "cargo:rbconfig_{}={}", key, rbconfig.get(key)); + cfg_capture!( + cap, + "cargo:rbconfig_{}={}", + key, + rbconfig.get(key).expect("key") + ); } if is_ruby_static_enabled(rbconfig) { @@ -227,13 +247,11 @@ fn export_cargo_cfg(rbconfig: &mut RbConfig, cap: &mut File) { } else { cfg_capture!(cap, "cargo:lib={}", rbconfig.libruby_so_name()); } - - cfg_capture!(cap, "cargo:libdir={}", rbconfig.get("libdir")); } fn rustc_cfg(rbconfig: &RbConfig, name: &str, key: &str) { println!("cargo:rustc-check-cfg=cfg({})", name); - if let Some(k) = rbconfig.get_optional(key) { + if let Some(k) = rbconfig.get(key) { println!("cargo:rustc-cfg={}=\"{}\"", name, k); } } @@ -242,6 +260,8 @@ fn enable_dynamic_lookup(rbconfig: &mut RbConfig) { // See https://github.com/oxidize-rb/rb-sys/issues/88 if cfg!(target_os = "macos") { rbconfig.push_dldflags("-Wl,-undefined,dynamic_lookup"); + } else if matches!(rbconfig.ruby_engine(), RubyEngine::TruffleRuby) { + rbconfig.push_dldflags("-Wl,-z,lazy"); } } diff --git a/crates/rb-sys/build/stable_api_config.rs b/crates/rb-sys/build/stable_api_config.rs index 55f58316..e92fbc5f 100644 --- a/crates/rb-sys/build/stable_api_config.rs +++ b/crates/rb-sys/build/stable_api_config.rs @@ -1,11 +1,15 @@ +use rb_sys_build::{RbConfig, RubyEngine}; + use crate::{ features::is_env_variable_defined, version::{Version, MIN_SUPPORTED_STABLE_VERSION}, }; use std::{convert::TryFrom, error::Error, path::Path}; -pub fn setup(current_ruby_version: Version) -> Result<(), Box> { - let strategy = Strategy::try_from(current_ruby_version)?; +pub fn setup(rb_config: &RbConfig) -> Result<(), Box> { + let ruby_version = Version::current(rb_config); + let ruby_engine = rb_config.ruby_engine(); + let strategy = Strategy::try_from((ruby_engine, ruby_version))?; strategy.apply()?; @@ -20,12 +24,24 @@ enum Strategy { Testing(Version), } -impl TryFrom for Strategy { +impl TryFrom<(RubyEngine, Version)> for Strategy { type Error = Box; - fn try_from(current_ruby_version: Version) -> Result { + fn try_from( + (engine, current_ruby_version): (RubyEngine, Version), + ) -> Result { let mut strategy = None; + match engine { + RubyEngine::TruffleRuby => { + return Ok(Strategy::CompiledOnly); + } + RubyEngine::JRuby => { + return Err("JRuby is not supported".into()); + } + RubyEngine::Mri => {} + } + if current_ruby_version.is_stable() { strategy = Some(Strategy::RustOnly(current_ruby_version)); } else { diff --git a/crates/rb-sys/build/version.rs b/crates/rb-sys/build/version.rs index fec4f455..9b36c689 100644 --- a/crates/rb-sys/build/version.rs +++ b/crates/rb-sys/build/version.rs @@ -22,10 +22,19 @@ impl Version { } pub fn current(rbconfig: &RbConfig) -> Version { - Self( - rbconfig.get("MAJOR").parse::().unwrap() as _, - rbconfig.get("MINOR").parse::().unwrap() as _, - ) + match (rbconfig.get("MAJOR"), rbconfig.get("MINOR")) { + (Some(major), Some(minor)) => { + Version::new(major.parse::().unwrap(), minor.parse::().unwrap()) + } + _ => { + // Try to parse out the first 3 components of the version string (for truffleruby) + let version_string = rbconfig.get("ruby_version").expect("ruby_version"); + let mut parts = version_string.split('.').map(|s| s.parse::()); + let major = parts.next().expect("major").unwrap(); + let minor = parts.next().expect("minor").unwrap(); + Version::new(major, minor) + } + } } #[allow(dead_code)] diff --git a/crates/rb-sys/src/hidden.rs b/crates/rb-sys/src/hidden.rs index 83fafb8f..86262c61 100644 --- a/crates/rb-sys/src/hidden.rs +++ b/crates/rb-sys/src/hidden.rs @@ -6,5 +6,6 @@ extern "C" { /// A pointer to the current Ruby VM. #[cfg(all(ruby_gt_2_4, ruby_lte_3_2))] + #[cfg(ruby_engine = "mri")] pub(crate) static ruby_current_vm_ptr: *mut crate::ruby_vm_t; } diff --git a/crates/rb-sys/src/macros.rs b/crates/rb-sys/src/macros.rs index f135244f..bddc64ab 100644 --- a/crates/rb-sys/src/macros.rs +++ b/crates/rb-sys/src/macros.rs @@ -266,6 +266,7 @@ pub unsafe fn RB_TYPE(value: VALUE) -> ruby_value_type { /// This function is unsafe because it could dereference a raw pointer when /// attemping to access the underlying [`RBasic`] struct. #[inline] +#[cfg(ruby_engine = "mri")] // truffleruby provides its own implementation pub unsafe fn RB_TYPE_P(obj: VALUE, ty: ruby_value_type) -> bool { api().type_p(obj, ty) } diff --git a/crates/rb-sys/src/ruby_abi_version.rs b/crates/rb-sys/src/ruby_abi_version.rs index 45aa201e..0389d4c1 100644 --- a/crates/rb-sys/src/ruby_abi_version.rs +++ b/crates/rb-sys/src/ruby_abi_version.rs @@ -20,6 +20,14 @@ pub extern "C" fn ruby_abi_version() -> std::os::raw::c_ulonglong { __RB_SYS_RUBY_ABI_VERSION } +#[doc(hidden)] +#[no_mangle] +#[allow(unused)] +#[cfg(ruby_engine = "truffleruby")] +pub extern "C" fn rb_tr_abi_version() -> *const std::os::raw::c_char { + crate::TRUFFLERUBY_ABI_VERSION.as_ptr() as *const _ +} + #[deprecated( since = "0.9.102", note = "You no longer need to invoke this macro, the `ruby_abi_version` function is defined automatically." diff --git a/crates/rb-sys/src/tracking_allocator.rs b/crates/rb-sys/src/tracking_allocator.rs index 37f905f2..802cc8a6 100644 --- a/crates/rb-sys/src/tracking_allocator.rs +++ b/crates/rb-sys/src/tracking_allocator.rs @@ -1,8 +1,6 @@ //! Support for reporting Rust memory usage to the Ruby GC. -use crate::{rb_gc_adjust_memory_usage, utils::is_ruby_vm_started}; use std::{ - alloc::{GlobalAlloc, Layout, System}, fmt::Formatter, sync::{ atomic::{AtomicIsize, Ordering}, @@ -10,104 +8,166 @@ use std::{ }, }; -/// A simple wrapper over [`std::alloc::System`] which reports memory usage to -/// the Ruby GC. This gives the GC a more accurate picture of the process' -/// memory usage so it can make better decisions about when to run. -#[derive(Debug)] -pub struct TrackingAllocator; - -impl TrackingAllocator { - /// Create a new [`TrackingAllocator`]. - #[allow(clippy::new_without_default)] - pub const fn new() -> Self { - Self - } - - /// Create a new [`TrackingAllocator`] with default values. - pub const fn default() -> Self { - Self::new() - } +#[cfg(ruby_engine = "mri")] +mod mri { + use crate::{rb_gc_adjust_memory_usage, utils::is_ruby_vm_started}; + use std::alloc::{GlobalAlloc, Layout, System}; + + /// A simple wrapper over [`System`] which reports memory usage to + /// the Ruby GC. This gives the GC a more accurate picture of the process' + /// memory usage so it can make better decisions about when to run. + #[derive(Debug)] + pub struct TrackingAllocator; + + impl TrackingAllocator { + /// Create a new [`TrackingAllocator`]. + #[allow(clippy::new_without_default)] + pub const fn new() -> Self { + Self + } - /// Adjust the memory usage reported to the Ruby GC by `delta`. Useful for - /// tracking allocations invisible to the Rust allocator, such as `mmap` or - /// direct `malloc` calls. - /// - /// # Example - /// ``` - /// use rb_sys::TrackingAllocator; - /// - /// // Allocate 1024 bytes of memory using `mmap` or `malloc`... - /// TrackingAllocator::adjust_memory_usage(1024); - /// - /// // ...and then after the memory is freed, adjust the memory usage again. - /// TrackingAllocator::adjust_memory_usage(-1024); - /// ``` - pub fn adjust_memory_usage(delta: isize) -> isize { - if delta == 0 { - return 0; + /// Create a new [`TrackingAllocator`] with default values. + pub const fn default() -> Self { + Self::new() } - #[cfg(target_pointer_width = "32")] - let delta = delta as i32; + /// Adjust the memory usage reported to the Ruby GC by `delta`. Useful for + /// tracking allocations invisible to the Rust allocator, such as `mmap` or + /// direct `malloc` calls. + /// + /// # Example + /// ``` + /// use rb_sys::TrackingAllocator; + /// + /// // Allocate 1024 bytes of memory using `mmap` or `malloc`... + /// TrackingAllocator::adjust_memory_usage(1024); + /// + /// // ...and then after the memory is freed, adjust the memory usage again. + /// TrackingAllocator::adjust_memory_usage(-1024); + /// ``` + #[inline] + pub fn adjust_memory_usage(delta: isize) -> isize { + if delta == 0 { + return 0; + } + + #[cfg(target_pointer_width = "32")] + let delta = delta as i32; - #[cfg(target_pointer_width = "64")] - let delta = delta as i64; + #[cfg(target_pointer_width = "64")] + let delta = delta as i64; - unsafe { - if is_ruby_vm_started() { - rb_gc_adjust_memory_usage(delta); - delta as isize - } else { - 0 + unsafe { + if is_ruby_vm_started() { + rb_gc_adjust_memory_usage(delta); + delta as isize + } else { + 0 + } } } } -} -unsafe impl GlobalAlloc for TrackingAllocator { - unsafe fn alloc(&self, layout: Layout) -> *mut u8 { - let ret = System.alloc(layout); - let delta = layout.size() as isize; + unsafe impl GlobalAlloc for TrackingAllocator { + #[inline] + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + let ret = System.alloc(layout); + let delta = layout.size() as isize; + + if !ret.is_null() && delta != 0 { + Self::adjust_memory_usage(delta); + } - if !ret.is_null() && delta != 0 { - Self::adjust_memory_usage(delta); + ret } - ret - } + #[inline] + unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { + let ret = System.alloc_zeroed(layout); + let delta = layout.size() as isize; + + if !ret.is_null() && delta != 0 { + Self::adjust_memory_usage(delta); + } + + ret + } - unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { - let ret = System.alloc_zeroed(layout); - let delta = layout.size() as isize; + #[inline] + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + System.dealloc(ptr, layout); + let delta = -(layout.size() as isize); - if !ret.is_null() && delta != 0 { - Self::adjust_memory_usage(delta); + if delta != 0 { + Self::adjust_memory_usage(delta); + } } - ret + #[inline] + unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { + let ret = System.realloc(ptr, layout, new_size); + let delta = new_size as isize - layout.size() as isize; + + if !ret.is_null() && delta != 0 { + Self::adjust_memory_usage(delta); + } + + ret + } } +} + +#[cfg(not(ruby_engine = "mri"))] +mod non_mri { + use std::alloc::{GlobalAlloc, Layout, System}; - unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { - System.dealloc(ptr, layout); - let delta = -(layout.size() as isize); + /// A simple wrapper over [`System`] as a fallback for non-MRI Ruby engines. + pub struct TrackingAllocator; + + impl TrackingAllocator { + #[allow(clippy::new_without_default)] + pub const fn new() -> Self { + Self + } - if delta != 0 { - Self::adjust_memory_usage(delta); + pub const fn default() -> Self { + Self::new() + } + + pub fn adjust_memory_usage(_delta: isize) -> isize { + 0 } } - unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { - let ret = System.realloc(ptr, layout, new_size); - let delta = new_size as isize - layout.size() as isize; + unsafe impl GlobalAlloc for TrackingAllocator { + #[inline] + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + System.alloc(layout) + } + + #[inline] + unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { + System.alloc_zeroed(layout) + } - if !ret.is_null() && delta != 0 { - Self::adjust_memory_usage(delta); + #[inline] + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + System.dealloc(ptr, layout) } - ret + #[inline] + unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { + System.realloc(ptr, layout, new_size) + } } } +#[cfg(ruby_engine = "mri")] +pub use mri::*; + +#[cfg(not(ruby_engine = "mri"))] +pub use non_mri::*; + /// Set the global allocator to [`TrackingAllocator`]. /// /// # Example @@ -132,7 +192,7 @@ struct MemsizeDelta(Arc); impl MemsizeDelta { fn new(delta: isize) -> Self { - TrackingAllocator::adjust_memory_usage(delta); + let delta = TrackingAllocator::adjust_memory_usage(delta); Self(Arc::new(AtomicIsize::new(delta))) } @@ -141,8 +201,8 @@ impl MemsizeDelta { return; } + let delta = TrackingAllocator::adjust_memory_usage(delta as _); self.0.fetch_add(delta as _, Ordering::SeqCst); - TrackingAllocator::adjust_memory_usage(delta as _); } fn sub(&self, delta: usize) { @@ -150,8 +210,8 @@ impl MemsizeDelta { return; } - self.0.fetch_sub(delta as isize, Ordering::SeqCst); - TrackingAllocator::adjust_memory_usage(-(delta as isize)); + let delta = TrackingAllocator::adjust_memory_usage(-(delta as isize)); + self.0.fetch_add(delta, Ordering::SeqCst); } fn get(&self) -> isize { diff --git a/crates/rb-sys/src/utils.rs b/crates/rb-sys/src/utils.rs index 0818b153..0dbf0038 100644 --- a/crates/rb-sys/src/utils.rs +++ b/crates/rb-sys/src/utils.rs @@ -19,11 +19,20 @@ /// ensure the VM hasn't stopped, which makes the function name a bit of a /// misnomer... but in actuality this function can only guarantee that the /// VM is started, not that it's still running. +#[allow(dead_code)] pub(crate) unsafe fn is_ruby_vm_started() -> bool { - #[cfg(all(ruby_gt_2_4, ruby_lte_3_2))] - let ret = !crate::hidden::ruby_current_vm_ptr.is_null(); + #[cfg(ruby_engine = "mri")] + let ret = { + #[cfg(all(ruby_gt_2_4, ruby_lte_3_2))] + let ret = !crate::hidden::ruby_current_vm_ptr.is_null(); - #[cfg(any(ruby_lte_2_4, ruby_gt_3_2))] + #[cfg(any(ruby_lte_2_4, ruby_gt_3_2))] + let ret = crate::rb_cBasicObject != 0; + + ret + }; + + #[cfg(ruby_engine = "truffleruby")] let ret = crate::rb_cBasicObject != 0; ret diff --git a/gem/lib/rb_sys/cargo_builder.rb b/gem/lib/rb_sys/cargo_builder.rb index 2198c97f..835e04b7 100644 --- a/gem/lib/rb_sys/cargo_builder.rb +++ b/gem/lib/rb_sys/cargo_builder.rb @@ -146,6 +146,11 @@ def platform_specific_rustc_args(dest_dir, flags = []) # See https://github.com/oxidize-rb/rb-sys/issues/88 dl_flag = "-Wl,-undefined,dynamic_lookup" flags += ["-C", "link-arg=#{dl_flag}"] unless makefile_config("DLDFLAGS")&.include?(dl_flag) + elsif RUBY_ENGINE == "truffleruby" + dl_flag = "-Wl,-z,lazy" + flags += ["-C", "link-arg=#{dl_flag}"] unless makefile_config("DLDFLAGS")&.include?(dl_flag) + # lazy binding requires RELRO to be off, see https://users.rust-lang.org/t/linux-executable-lazy-loading/65621/2 + flags += ["-C", "relro-level=off"] end if musl? diff --git a/gem/lib/rb_sys/mkmf.rb b/gem/lib/rb_sys/mkmf.rb index 2d5fd4b7..3e6883d3 100644 --- a/gem/lib/rb_sys/mkmf.rb +++ b/gem/lib/rb_sys/mkmf.rb @@ -102,7 +102,7 @@ def create_rust_makefile(target, &blk) RUSTLIBDIR = $(RB_SYS_FULL_TARGET_DIR)/$(RB_SYS_CARGO_PROFILE_DIR) RUSTLIB = $(RUSTLIBDIR)/$(SOEXT_PREFIX)$(TARGET_NAME).$(SOEXT) TIMESTAMP_DIR = . - POSTLINK = #{RbConfig::CONFIG["POSTLINK"] || "$(ECHO) skipping postlink (not found)"} + POSTLINK = #{RbConfig::CONFIG["POSTLINK"] || "$(ECHO) skipping postlink"} CLEANOBJS = $(RUSTLIBDIR) $(RB_SYS_BUILD_DIR) CLEANLIBS = $(DLLIB) $(RUSTLIB) diff --git a/readme.md b/readme.md index 85c9e11d..4a43516d 100644 --- a/readme.md +++ b/readme.md @@ -67,8 +67,8 @@ On Linux, you can install it with `apt-get install libclang-dev`. We support cross compilation to the following platforms (this information is also available in the [`./data`](./data) directory for automation purposes): -| Platform | Supported | Docker Image | -| ------------------ | --------- | ----------------------------------------------- | +| Platform | Supported | Docker Image | +| ------------------ | --------- | ------------------------------------------------ | | x86_64-linux | ✅ | [`rbsys/x86_64-linux:0.9.102`][docker-hub] | | x86_64-linux-musl | ✅ | [`rbsys/x86_64-linux-musl:0.9.102`][docker-hub] | | aarch64-linux | ✅ | [`rbsys/aarch64-linux:0.9.102`][docker-hub] | @@ -77,7 +77,8 @@ directory for automation purposes): | arm64-darwin | ✅ | [`rbsys/arm64-darwin:0.9.102`][docker-hub] | | x64-mingw32 | ✅ | [`rbsys/x64-mingw32:0.9.102`][docker-hub] | | x64-mingw-ucrt | ✅ | [`rbsys/x64-mingw-ucrt:0.9.102`][docker-hub] | -| mswin | ✅ | not available on Docker | +| mswin | ✅ | not available on Docker | +| truffleruby | ✅ | not available on Docker | ## Getting Help