diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e1ee451..a1649743 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: fail-fast: false matrix: # Test against all versions supported by rubygems - ruby_version: ["2.6", "2.7", "3.0", "3.1", "3.2", "3.3"] + ruby_version: ["2.7", "3.0", "3.1", "3.2", "3.3", "3.4.0-preview2"] sys: - os: ubuntu-latest rust_toolchain: ${{ needs.fetch_ci_data.outputs.minimum-supported-rust-version }} @@ -53,6 +53,14 @@ jobs: sys: os: macos-latest rust_toolchain: stable + - ruby_version: "ruby-debug" + sys: + os: ubuntu-24.04 + rust_toolchain: stable + - ruby_version: mswin + sys: + os: windows-2022 + rust_toolchain: stable-x86_64-pc-windows-msvc exclude: # Missing symbols for some reason, need to fix - ruby_version: "2.6" @@ -67,12 +75,10 @@ jobs: sys: os: macos-latest rust_toolchain: stable - # MSC version mismatch, need to fix - # include: - # - ruby_version: mswin - # sys: - # os: windows-2022 - # rust_toolchain: stable-x86_64-pc-windows-msvc + - ruby_version: "3.4.0-preview2" + sys: + os: windows-2022 + rust_toolchain: stable runs-on: ${{ matrix.sys.os }} steps: - uses: actions/checkout@v4 diff --git a/crates/rb-sys-build/src/cc.rs b/crates/rb-sys-build/src/cc.rs index 52590456..4727f481 100644 --- a/crates/rb-sys-build/src/cc.rs +++ b/crates/rb-sys-build/src/cc.rs @@ -282,7 +282,27 @@ fn get_tool(env_var: &str, default: &str) -> Command { let mut tool_args = shellsplit(tool_args).into_iter(); let tool = tool_args.next().unwrap_or_else(|| default.to_string()); - let mut cmd = if Path::new(&tool).is_file() { + fn tool_exists(tool_name: &str) -> std::io::Result { + let path = PathBuf::from(tool_name); + + if path.is_file() { + return Ok(true); + } + + match Command::new(tool_name).spawn() { + Ok(_) => Ok(true), + Err(e) => { + if e.kind() == std::io::ErrorKind::NotFound { + Ok(false) + } else { + Err(e) + } + } + } + } + + let mut cmd = if tool_exists(&tool).unwrap_or(false) { + debug_log!("[INFO] using {tool} for {env_var}"); new_command(&tool) } else { debug_log!("[WARN] {tool} tool not found, falling back to {default}"); diff --git a/crates/rb-sys-test-helpers/src/ruby_exception.rs b/crates/rb-sys-test-helpers/src/ruby_exception.rs index 2b5eef7e..79adf934 100644 --- a/crates/rb-sys-test-helpers/src/ruby_exception.rs +++ b/crates/rb-sys-test-helpers/src/ruby_exception.rs @@ -129,7 +129,10 @@ mod tests { assert_eq!("RuntimeError", exception.classname()); assert_eq!("oh no", exception.message().unwrap()); #[cfg(ruby_gt_2_4)] - assert!(exception.full_message().unwrap().contains("eval:1:in `"),); + { + let message = exception.full_message().unwrap(); + assert!(message.contains("eval:1:in "), "message: {}", message); + } }) } } 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 da9b5c08..168e7cc2 100644 --- a/crates/rb-sys-test-helpers/src/ruby_test_executor.rs +++ b/crates/rb-sys-test-helpers/src/ruby_test_executor.rs @@ -1,5 +1,6 @@ use std::error::Error; use std::panic; +use std::ptr::addr_of_mut; use std::sync::mpsc::{self, SyncSender}; use std::sync::Once; use std::thread::{self, JoinHandle}; @@ -10,7 +11,7 @@ use crate::once_cell::OnceCell; use rb_sys::rb_ext_ractor_safe; use rb_sys::{ rb_errinfo, rb_inspect, rb_protect, rb_set_errinfo, rb_string_value_cstr, ruby_exec_node, - ruby_process_options, ruby_setup, Qnil, VALUE, + ruby_init_stack, ruby_process_options, ruby_setup, Qnil, VALUE, }; static mut GLOBAL_EXECUTOR: OnceCell = OnceCell::new(); @@ -141,6 +142,9 @@ pub unsafe fn setup_ruby_unguarded() { rb_sys::rb_w32_sysinit(&mut argc, &mut argv); } + let mut stack_marker: VALUE = 0; + ruby_init_stack(addr_of_mut!(stack_marker) as *mut _); + match ruby_setup() { 0 => {} code => panic!("Failed to setup Ruby (error code: {})", code), @@ -150,7 +154,7 @@ pub unsafe fn setup_ruby_unguarded() { let mut argv: [*mut i8; 3] = [ "ruby\0".as_ptr() as _, "-e\0".as_ptr() as _, - "nil\0".as_ptr() as _, + "\0".as_ptr() as _, ]; ruby_process_options(argv.len() as _, argv.as_mut_ptr() as _) as _ diff --git a/crates/rb-sys-tests/src/stable_api_test.rs b/crates/rb-sys-tests/src/stable_api_test.rs index fe6c05d8..cff4203c 100644 --- a/crates/rb-sys-tests/src/stable_api_test.rs +++ b/crates/rb-sys-tests/src/stable_api_test.rs @@ -8,6 +8,8 @@ macro_rules! parity_test { use rb_sys::stable_api; let data = $data_factory; + assert_ne!(stable_api::get_default().version(), (0, 0)); + #[allow(unused)] let rust_result = unsafe { stable_api::get_default().$func(data) }; #[allow(unused_unsafe)] diff --git a/crates/rb-sys/build/main.rs b/crates/rb-sys/build/main.rs index 6daabacd..890eeb35 100644 --- a/crates/rb-sys/build/main.rs +++ b/crates/rb-sys/build/main.rs @@ -13,7 +13,7 @@ use std::{ }; use version::Version; -const SUPPORTED_RUBY_VERSIONS: [Version; 9] = [ +const SUPPORTED_RUBY_VERSIONS: [Version; 10] = [ Version::new(2, 3), Version::new(2, 4), Version::new(2, 5), @@ -23,6 +23,7 @@ const SUPPORTED_RUBY_VERSIONS: [Version; 9] = [ Version::new(3, 1), Version::new(3, 2), Version::new(3, 3), + Version::new(3, 4), ]; fn main() { @@ -55,7 +56,10 @@ fn main() { export_cargo_cfg(&mut rbconfig, &mut cfg_capture_file); #[cfg(feature = "stable-api")] - stable_api_config::setup(&rbconfig).expect("could not setup stable API"); + if let Err(e) = stable_api_config::setup(&rbconfig) { + eprintln!("Failed to setup stable API: {}", e); + std::process::exit(1); + } if is_link_ruby_enabled() { link_libruby(&mut rbconfig); diff --git a/crates/rb-sys/build/version.rs b/crates/rb-sys/build/version.rs index 9b36c689..98e099d0 100644 --- a/crates/rb-sys/build/version.rs +++ b/crates/rb-sys/build/version.rs @@ -1,7 +1,7 @@ use crate::RbConfig; #[allow(dead_code)] -pub const LATEST_STABLE_VERSION: Version = Version::new(3, 3); +pub const LATEST_STABLE_VERSION: Version = Version::new(3, 4); #[allow(dead_code)] pub const MIN_SUPPORTED_STABLE_VERSION: Version = Version::new(2, 6); diff --git a/crates/rb-sys/src/special_consts.rs b/crates/rb-sys/src/special_consts.rs index 6e8e5361..e12a6b6c 100644 --- a/crates/rb-sys/src/special_consts.rs +++ b/crates/rb-sys/src/special_consts.rs @@ -7,6 +7,8 @@ //! Makes it easier to reference important Ruby constants, without having to dig //! around in bindgen's output. +use std::ffi::c_long; + use crate::{ruby_special_consts, VALUE}; pub const Qfalse: ruby_special_consts = ruby_special_consts::RUBY_Qfalse; @@ -15,6 +17,8 @@ pub const Qnil: ruby_special_consts = ruby_special_consts::RUBY_Qnil; pub const Qundef: ruby_special_consts = ruby_special_consts::RUBY_Qundef; pub const IMMEDIATE_MASK: ruby_special_consts = ruby_special_consts::RUBY_IMMEDIATE_MASK; pub const FIXNUM_FLAG: ruby_special_consts = ruby_special_consts::RUBY_FIXNUM_FLAG; +pub const FIXNUM_MIN: c_long = c_long::MIN / 2; +pub const FIXNUM_MAX: c_long = c_long::MAX / 2; pub const FLONUM_MASK: ruby_special_consts = ruby_special_consts::RUBY_FLONUM_MASK; pub const FLONUM_FLAG: ruby_special_consts = ruby_special_consts::RUBY_FLONUM_FLAG; pub const SYMBOL_FLAG: ruby_special_consts = ruby_special_consts::RUBY_SYMBOL_FLAG; diff --git a/crates/rb-sys/src/stable_api.rs b/crates/rb-sys/src/stable_api.rs index 72bb0386..d2c4bd57 100644 --- a/crates/rb-sys/src/stable_api.rs +++ b/crates/rb-sys/src/stable_api.rs @@ -15,6 +15,13 @@ use crate::VALUE; use std::os::raw::{c_char, c_long}; pub trait StableApiDefinition { + const VERSION_MAJOR: u32; + const VERSION_MINOR: u32; + + fn version(&self) -> (u32, u32) { + (Self::VERSION_MAJOR, Self::VERSION_MINOR) + } + /// Get the length of a Ruby string (akin to `RSTRING_LEN`). /// /// # Safety @@ -139,10 +146,20 @@ use compiled as api; #[cfg_attr(ruby_eq_3_1, path = "stable_api/ruby_3_1.rs")] #[cfg_attr(ruby_eq_3_2, path = "stable_api/ruby_3_2.rs")] #[cfg_attr(ruby_eq_3_3, path = "stable_api/ruby_3_3.rs")] +#[cfg_attr(ruby_eq_3_4, path = "stable_api/ruby_3_4.rs")] mod rust; #[cfg(not(stable_api_export_compiled_as_api))] use rust as api; +impl std::fmt::Debug for api::Definition { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("StableApiDefinition") + .field("VERSION_MAJOR", &api::Definition::VERSION_MAJOR) + .field("VERSION_MINOR", &api::Definition::VERSION_MINOR) + .finish() + } +} + /// Get the default stable API definition for the current Ruby version. pub const fn get_default() -> &'static api::Definition { const API: api::Definition = api::Definition {}; diff --git a/crates/rb-sys/src/stable_api/compiled.rs b/crates/rb-sys/src/stable_api/compiled.rs index 18aabf2f..0f5b8694 100644 --- a/crates/rb-sys/src/stable_api/compiled.rs +++ b/crates/rb-sys/src/stable_api/compiled.rs @@ -62,6 +62,9 @@ extern "C" { pub struct Definition; impl StableApiDefinition for Definition { + const VERSION_MAJOR: u32 = 0; + const VERSION_MINOR: u32 = 0; + #[inline] unsafe fn rstring_len(&self, obj: VALUE) -> std::os::raw::c_long { impl_rstring_len(obj) diff --git a/crates/rb-sys/src/stable_api/ruby_2_6.rs b/crates/rb-sys/src/stable_api/ruby_2_6.rs index 045a695e..1e130479 100644 --- a/crates/rb-sys/src/stable_api/ruby_2_6.rs +++ b/crates/rb-sys/src/stable_api/ruby_2_6.rs @@ -14,6 +14,9 @@ compile_error!("This file should only be included in Ruby 2.6 builds"); pub struct Definition; impl StableApiDefinition for Definition { + const VERSION_MAJOR: u32 = 2; + const VERSION_MINOR: u32 = 6; + #[inline] unsafe fn rstring_len(&self, obj: VALUE) -> c_long { assert!(self.type_p(obj, crate::ruby_value_type::RUBY_T_STRING)); diff --git a/crates/rb-sys/src/stable_api/ruby_2_7.rs b/crates/rb-sys/src/stable_api/ruby_2_7.rs index 0a942f20..c48ea929 100644 --- a/crates/rb-sys/src/stable_api/ruby_2_7.rs +++ b/crates/rb-sys/src/stable_api/ruby_2_7.rs @@ -11,6 +11,9 @@ compile_error!("This file should only be included in Ruby 2.7 builds"); pub struct Definition; impl StableApiDefinition for Definition { + const VERSION_MAJOR: u32 = 2; + const VERSION_MINOR: u32 = 7; + #[inline] unsafe fn rstring_len(&self, obj: VALUE) -> c_long { assert!(self.type_p(obj, crate::ruby_value_type::RUBY_T_STRING)); diff --git a/crates/rb-sys/src/stable_api/ruby_3_0.rs b/crates/rb-sys/src/stable_api/ruby_3_0.rs index 8901c31a..8df66d18 100644 --- a/crates/rb-sys/src/stable_api/ruby_3_0.rs +++ b/crates/rb-sys/src/stable_api/ruby_3_0.rs @@ -11,6 +11,9 @@ compile_error!("This file should only be included in Ruby 3.0 builds"); pub struct Definition; impl StableApiDefinition for Definition { + const VERSION_MAJOR: u32 = 3; + const VERSION_MINOR: u32 = 0; + #[inline] unsafe fn rstring_len(&self, obj: VALUE) -> c_long { unsafe { diff --git a/crates/rb-sys/src/stable_api/ruby_3_1.rs b/crates/rb-sys/src/stable_api/ruby_3_1.rs index 3bd7b501..ca11f46c 100644 --- a/crates/rb-sys/src/stable_api/ruby_3_1.rs +++ b/crates/rb-sys/src/stable_api/ruby_3_1.rs @@ -11,6 +11,9 @@ compile_error!("This file should only be included in Ruby 3.1 builds"); pub struct Definition; impl StableApiDefinition for Definition { + const VERSION_MAJOR: u32 = 3; + const VERSION_MINOR: u32 = 1; + #[inline] unsafe fn rstring_len(&self, obj: VALUE) -> c_long { assert!(self.type_p(obj, crate::ruby_value_type::RUBY_T_STRING)); diff --git a/crates/rb-sys/src/stable_api/ruby_3_2.rs b/crates/rb-sys/src/stable_api/ruby_3_2.rs index 934272fc..4f9ce63c 100644 --- a/crates/rb-sys/src/stable_api/ruby_3_2.rs +++ b/crates/rb-sys/src/stable_api/ruby_3_2.rs @@ -11,6 +11,9 @@ compile_error!("This file should only be included in Ruby 3.2 builds"); pub struct Definition; impl StableApiDefinition for Definition { + const VERSION_MAJOR: u32 = 3; + const VERSION_MINOR: u32 = 2; + #[inline] unsafe fn rstring_len(&self, obj: VALUE) -> c_long { assert!(self.type_p(obj, crate::ruby_value_type::RUBY_T_STRING)); diff --git a/crates/rb-sys/src/stable_api/ruby_3_3.rs b/crates/rb-sys/src/stable_api/ruby_3_3.rs index 00550b88..67afd05b 100644 --- a/crates/rb-sys/src/stable_api/ruby_3_3.rs +++ b/crates/rb-sys/src/stable_api/ruby_3_3.rs @@ -11,6 +11,9 @@ compile_error!("This file should only be included in Ruby 3.3 builds"); pub struct Definition; impl StableApiDefinition for Definition { + const VERSION_MAJOR: u32 = 3; + const VERSION_MINOR: u32 = 3; + #[inline] unsafe fn rstring_len(&self, obj: VALUE) -> c_long { assert!(self.type_p(obj, crate::ruby_value_type::RUBY_T_STRING)); diff --git a/crates/rb-sys/src/stable_api/ruby_3_4.rs b/crates/rb-sys/src/stable_api/ruby_3_4.rs new file mode 100644 index 00000000..4d1b4dd7 --- /dev/null +++ b/crates/rb-sys/src/stable_api/ruby_3_4.rs @@ -0,0 +1,220 @@ +use super::StableApiDefinition; +use crate::{ + internal::{RArray, RString}, + value_type, VALUE, +}; +use std::os::raw::{c_char, c_long}; + +#[cfg(not(ruby_eq_3_4))] +compile_error!("This file should only be included in Ruby 3.3 builds"); + +pub struct Definition; + +impl StableApiDefinition for Definition { + const VERSION_MAJOR: u32 = 3; + const VERSION_MINOR: u32 = 4; + + #[inline] + unsafe fn rstring_len(&self, obj: VALUE) -> c_long { + assert!(self.type_p(obj, crate::ruby_value_type::RUBY_T_STRING)); + + let rstring: &RString = &*(obj as *const RString); + rstring.len + } + + #[inline] + unsafe fn rstring_ptr(&self, obj: VALUE) -> *const c_char { + assert!(self.type_p(obj, crate::ruby_value_type::RUBY_T_STRING)); + + let rstring: &RString = &*(obj as *const RString); + let flags = rstring.basic.flags; + let is_heap = (flags & crate::ruby_rstring_flags::RSTRING_NOEMBED as VALUE) != 0; + let ptr = if !is_heap { + std::ptr::addr_of!(rstring.as_.embed.ary) as *const _ + } else { + rstring.as_.heap.ptr + }; + + assert!(!ptr.is_null()); + + ptr + } + + #[inline] + unsafe fn rarray_len(&self, obj: VALUE) -> c_long { + assert!(self.type_p(obj, value_type::RUBY_T_ARRAY)); + + let rarray: &RArray = &*(obj as *const RArray); + let flags = rarray.basic.flags; + let is_embedded = (flags & crate::ruby_rarray_flags::RARRAY_EMBED_FLAG as VALUE) != 0; + + if is_embedded { + let mut f = rarray.basic.flags; + f &= crate::ruby_rarray_flags::RARRAY_EMBED_LEN_MASK as VALUE; + f >>= crate::ruby_rarray_consts::RARRAY_EMBED_LEN_SHIFT as VALUE; + f as c_long + } else { + rarray.as_.heap.len + } + } + + #[inline] + unsafe fn rarray_const_ptr(&self, obj: VALUE) -> *const VALUE { + assert!(self.type_p(obj, value_type::RUBY_T_ARRAY)); + + let rarray: &RArray = &*(obj as *const RArray); + let flags = rarray.basic.flags; + let is_embedded = (flags & crate::ruby_rarray_flags::RARRAY_EMBED_FLAG as VALUE) != 0; + let ptr = if is_embedded { + std::ptr::addr_of!(rarray.as_.ary) as *const _ + } else { + rarray.as_.heap.ptr + }; + + assert!(!ptr.is_null()); + + ptr + } + + #[inline] + fn special_const_p(&self, value: VALUE) -> bool { + let is_immediate = (value) & (crate::special_consts::IMMEDIATE_MASK as VALUE) != 0; + let test = (value & !(crate::Qnil as VALUE)) != 0; + + is_immediate || !test + } + + #[inline] + unsafe fn builtin_type(&self, obj: VALUE) -> crate::ruby_value_type { + let rbasic = obj as *const crate::RBasic; + let ret: u32 = ((*rbasic).flags & crate::ruby_value_type::RUBY_T_MASK as VALUE) as _; + + std::mem::transmute::<_, crate::ruby_value_type>(ret) + } + + #[inline] + fn nil_p(&self, obj: VALUE) -> bool { + obj == (crate::Qnil as VALUE) + } + + #[inline] + fn fixnum_p(&self, obj: VALUE) -> bool { + (obj & crate::FIXNUM_FLAG as VALUE) != 0 + } + + #[inline] + fn static_sym_p(&self, obj: VALUE) -> bool { + let mask = !(VALUE::MAX << crate::ruby_special_consts::RUBY_SPECIAL_SHIFT as VALUE); + (obj & mask) == crate::ruby_special_consts::RUBY_SYMBOL_FLAG as VALUE + } + + #[inline] + fn flonum_p(&self, obj: VALUE) -> bool { + #[cfg(ruby_use_flonum = "true")] + let ret = (obj & crate::FLONUM_MASK as VALUE) == crate::FLONUM_FLAG as VALUE; + + #[cfg(not(ruby_use_flonum = "true"))] + let ret = false; + + ret + } + + #[inline] + fn immediate_p(&self, obj: VALUE) -> bool { + (obj & crate::special_consts::IMMEDIATE_MASK as VALUE) != 0 + } + + #[inline] + fn rb_test(&self, obj: VALUE) -> bool { + (obj & !(crate::Qnil as VALUE)) != 0 + } + + #[inline] + unsafe fn type_p(&self, obj: VALUE, t: crate::ruby_value_type) -> bool { + use crate::ruby_special_consts::*; + use crate::ruby_value_type::*; + + if t == RUBY_T_TRUE { + obj == RUBY_Qtrue as _ + } else if t == RUBY_T_FALSE { + obj == RUBY_Qfalse as _ + } else if t == RUBY_T_NIL { + obj == RUBY_Qnil as _ + } else if t == RUBY_T_UNDEF { + obj == RUBY_Qundef as _ + } else if t == RUBY_T_FIXNUM { + self.fixnum_p(obj) + } else if t == RUBY_T_SYMBOL { + self.symbol_p(obj) + } else if t == RUBY_T_FLOAT { + self.float_type_p(obj) + } else if self.special_const_p(obj) { + false + } else if t == self.builtin_type(obj) { + true + } else { + t == self.rb_type(obj) + } + } + + #[inline] + unsafe fn symbol_p(&self, obj: VALUE) -> bool { + self.static_sym_p(obj) || self.dynamic_sym_p(obj) + } + + #[inline] + unsafe fn float_type_p(&self, obj: VALUE) -> bool { + if self.flonum_p(obj) { + true + } else if self.special_const_p(obj) { + false + } else { + self.builtin_type(obj) == value_type::RUBY_T_FLOAT + } + } + + #[inline] + unsafe fn rb_type(&self, obj: VALUE) -> crate::ruby_value_type { + use crate::ruby_special_consts::*; + use crate::ruby_value_type::*; + + if !self.special_const_p(obj) { + self.builtin_type(obj) + } else if obj == RUBY_Qfalse as _ { + RUBY_T_FALSE + } else if obj == RUBY_Qnil as _ { + RUBY_T_NIL + } else if obj == RUBY_Qtrue as _ { + RUBY_T_TRUE + } else if obj == RUBY_Qundef as _ { + RUBY_T_UNDEF + } else if self.fixnum_p(obj) { + RUBY_T_FIXNUM + } else if self.static_sym_p(obj) { + RUBY_T_SYMBOL + } else { + debug_assert!(self.flonum_p(obj)); + RUBY_T_FLOAT + } + } + + #[inline] + unsafe fn dynamic_sym_p(&self, obj: VALUE) -> bool { + if self.special_const_p(obj) { + false + } else { + self.builtin_type(obj) == value_type::RUBY_T_SYMBOL + } + } + + #[inline] + unsafe fn integer_type_p(&self, obj: VALUE) -> bool { + if self.fixnum_p(obj) { + true + } else if self.special_const_p(obj) { + false + } else { + self.builtin_type(obj) == value_type::RUBY_T_BIGNUM + } + } +}