From 009e62b0e5929c77dc32c4e6058d79e888b2d1ee Mon Sep 17 00:00:00 2001 From: Ian Ker-Seymer Date: Wed, 2 Aug 2023 21:02:39 -0400 Subject: [PATCH] Add `rb_gc_guard!` macro with same behavior as `RB_GC_GUARD` (#221) * Bump `rb-sys-test-helpers` to v0.1.4 * Add `rb_gc_guard!` macro * Ensure exact ASM is generated * Volatile writes for protect in test helpers * Safer version of RubyException in test helpers * Run all tests with opt-level=3 * Don't run memory tests on windows just yet (until we have stable macros' * Hardmode test for rb_gc_guard * Type sig fixes * Feature flag it * Disabe rb_gc_guard in test helpers for now * Bump msrv to 1.59 * Bump msrv to 1.59 * Remove ruby-lsp dep in gemfile * Dont attempt to upload entire target dir on cargo test fail * Remove rust toolchain file * Avoid double panicking in test helper shutdown * Increase timeout * Follow the rules of MaybeUninit more closely * Fixes * f * Remove unneeded feature flag * Restrict rake-compiler version for now --- .github/workflows/ci.yml | 2 +- Cargo.lock | 2 +- Gemfile | 2 +- crates/rb-sys-test-helpers-macros/src/lib.rs | 28 ++++--- crates/rb-sys-test-helpers/Cargo.toml | 2 +- crates/rb-sys-test-helpers/src/lib.rs | 52 ++++++------ .../rb-sys-test-helpers/src/ruby_exception.rs | 20 +++-- .../src/ruby_test_executor.rs | 81 ++++++++++--------- crates/rb-sys-tests/src/lib.rs | 3 + crates/rb-sys-tests/src/memory_test.rs | 56 +++++++++++++ .../src/tracking_allocator_test.rs | 2 +- crates/rb-sys/src/lib.rs | 2 + crates/rb-sys/src/memory.rs | 53 ++++++++++++ crates/rb-sys/src/stable_api.rs | 2 +- readme.md | 16 ++-- 15 files changed, 227 insertions(+), 96 deletions(-) create mode 100644 crates/rb-sys-tests/src/memory_test.rs create mode 100644 crates/rb-sys/src/memory.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4906b18..498c575e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,7 +88,7 @@ jobs: - name: ๐Ÿงช Cargo test shell: bash - run: script/ci/upload-on-failure.sh "bundle exec rake test:cargo" "cargo-test" "./target" + run: bundle exec rake test:cargo - name: ๐Ÿ’Ž Gem test run: bundle exec rake test:gem diff --git a/Cargo.lock b/Cargo.lock index 997acf37..4cc97983 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -257,7 +257,7 @@ version = "0.1.2" [[package]] name = "rb-sys-test-helpers" -version = "0.1.3" +version = "0.1.4" dependencies = [ "rb-sys", "rb-sys-env", diff --git a/Gemfile b/Gemfile index c69755e6..bd0d5896 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,7 @@ gemspec path: "examples/rust_reverse" gem "rake", "~> 13.0" gem "minitest", "5.15.0" -gem "rake-compiler", "~> 1.2.0" +gem "rake-compiler", "~> 1.2.0", "< 1.2.4" # Small bug in 1.2.4 that breaks Ruby 2.5 gem "yard" if RUBY_VERSION >= "2.6.0" diff --git a/crates/rb-sys-test-helpers-macros/src/lib.rs b/crates/rb-sys-test-helpers-macros/src/lib.rs index 3c545439..1d04293e 100644 --- a/crates/rb-sys-test-helpers-macros/src/lib.rs +++ b/crates/rb-sys-test-helpers-macros/src/lib.rs @@ -62,24 +62,24 @@ pub fn ruby_test(args: TokenStream, input: TokenStream) -> TokenStream { let vis = input.vis; let sig = &input.sig; - let block = quote! { - let ret = { - #block; - }; - rb_sys_test_helpers::trigger_full_gc!(); - ret - }; - let block = if gc_stress { quote! { rb_sys_test_helpers::with_gc_stress(|| { #block - }); + }) } } else { quote! { #block } }; + let block = quote! { + let ret = { + #block + }; + rb_sys_test_helpers::trigger_full_gc!(); + ret + }; + let test_fn = quote! { #[test] #(#attrs)* @@ -89,8 +89,7 @@ pub fn ruby_test(args: TokenStream, input: TokenStream) -> TokenStream { #block }); - match result { - Ok(_) => (), + let ret = match result { Err(err) => { match std::env::var("RUST_BACKTRACE") { Ok(val) if val == "1" => { @@ -101,10 +100,13 @@ pub fn ruby_test(args: TokenStream, input: TokenStream) -> TokenStream { }, _ => (), } - panic!("{}", err.inspect()); + Err(err) }, + Ok(v) => Ok(v), }; - }); + + ret + }).expect("test execution failure").expect("ruby exception"); } }; diff --git a/crates/rb-sys-test-helpers/Cargo.toml b/crates/rb-sys-test-helpers/Cargo.toml index 93085e52..295940f4 100644 --- a/crates/rb-sys-test-helpers/Cargo.toml +++ b/crates/rb-sys-test-helpers/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rb-sys-test-helpers" -version = "0.1.3" +version = "0.1.4" edition = "2018" description = "Helpers for testing Ruby extensions from Rust" homepage = "https://github.com/oxidize-rb/rb-sys" diff --git a/crates/rb-sys-test-helpers/src/lib.rs b/crates/rb-sys-test-helpers/src/lib.rs index 8c909135..399d94eb 100644 --- a/crates/rb-sys-test-helpers/src/lib.rs +++ b/crates/rb-sys-test-helpers/src/lib.rs @@ -7,7 +7,7 @@ mod utils; use rb_sys::{rb_errinfo, rb_intern, rb_set_errinfo, Qnil, VALUE}; use ruby_test_executor::global_executor; -use std::{mem::MaybeUninit, panic::UnwindSafe}; +use std::{error::Error, mem::MaybeUninit, panic::UnwindSafe}; pub use rb_sys_test_helpers_macros::*; pub use ruby_exception::RubyException; @@ -39,9 +39,10 @@ pub use utils::*; /// assert_eq!(result, "hello world"); /// }); /// ``` -pub fn with_ruby_vm(f: F) +pub fn with_ruby_vm(f: F) -> Result> where - F: FnOnce() + UnwindSafe + Send + 'static, + R: Send + 'static, + F: FnOnce() -> R + UnwindSafe + Send + 'static, { global_executor().run_test(f) } @@ -64,7 +65,11 @@ where /// assert_eq!(hello_world, "hello world"); /// }); /// ``` -pub fn with_gc_stress(f: impl FnOnce() -> T + std::panic::UnwindSafe) -> T { +pub fn with_gc_stress(f: F) -> R +where + R: Send + 'static, + F: FnOnce() -> R + UnwindSafe + Send + 'static, +{ unsafe { let stress_intern = rb_intern("stress\0".as_ptr() as _); let stress_eq_intern = rb_intern("stress=\0".as_ptr() as _); @@ -104,32 +109,28 @@ where F: FnMut() -> T + std::panic::UnwindSafe, { unsafe extern "C" fn ffi_closure T>(args: VALUE) -> VALUE { - let args: *mut (Option<*mut F>, Option>) = args as _; - let args = &mut *args; - let (func, outbuf) = args; + let args: *mut (Option<*mut F>, *mut Option) = args as _; + let args = *args; + let (mut func, outbuf) = args; let func = func.take().unwrap(); let func = &mut *func; - let mut outbuf = outbuf.take().unwrap(); - let result = func(); - outbuf.as_mut_ptr().write(result); - - outbuf.as_ptr() as _ + outbuf.write_volatile(Some(result)); + outbuf as _ } unsafe { let mut state = 0; let func_ref = &Some(f) as *const _; - let outbuf_ref = &MaybeUninit::uninit() as *const MaybeUninit; - let args = &(Some(func_ref), Some(outbuf_ref)) as *const _ as VALUE; - let outbuf_ptr = rb_sys::rb_protect(Some(ffi_closure::), args, &mut state); - let outbuf_ptr: *const MaybeUninit = outbuf_ptr as _; + let mut outbuf: MaybeUninit> = MaybeUninit::new(None); + let args = &(Some(func_ref), outbuf.as_mut_ptr() as *mut _) as *const _ as VALUE; + rb_sys::rb_protect(Some(ffi_closure::), args, &mut state); if state == 0 { - if let Some(result) = outbuf_ptr.as_ref() { - Ok(result.as_ptr().read()) + if outbuf.as_mut_ptr().read_volatile().is_some() { + Ok(outbuf.assume_init().expect("unreachable")) } else { - panic!("rb_protect returned a null pointer") + Err(RubyException::new(rb_errinfo())) } } else { let err = rb_errinfo(); @@ -144,12 +145,12 @@ mod tests { use super::*; #[test] - fn test_protect_returns_correct_value() { - with_ruby_vm(|| { - let result = protect(|| "my val"); + fn test_protect_returns_correct_value() -> Result<(), Box> { + let ret = with_ruby_vm(|| protect(|| "my val"))?; + + assert_eq!(ret, Ok("my val")); - assert_eq!(result, Ok("my val")); - }); + Ok(()) } #[test] @@ -160,6 +161,7 @@ mod tests { }); assert!(result.is_err()); - }); + }) + .unwrap(); } } diff --git a/crates/rb-sys-test-helpers/src/ruby_exception.rs b/crates/rb-sys-test-helpers/src/ruby_exception.rs index 73127865..2b5eef7e 100644 --- a/crates/rb-sys-test-helpers/src/ruby_exception.rs +++ b/crates/rb-sys-test-helpers/src/ruby_exception.rs @@ -8,7 +8,7 @@ use std::ffi::CStr; /// A simple wrapper around a Ruby exception that provides some convenience /// methods for testing. -#[derive(Clone, Copy, Eq, PartialEq)] +#[derive(Clone, Eq, PartialEq)] pub struct RubyException { value: VALUE, } @@ -20,7 +20,7 @@ impl RubyException { } /// Get the message of the Ruby exception. - pub fn message(self) -> Option { + pub fn message(&self) -> Option { unsafe { rb_funcall_typed!(self.value, "message", [], RUBY_T_STRING) .map(|mut message| rstring_to_string!(message)) @@ -28,7 +28,7 @@ impl RubyException { } /// Get the full message of the Ruby exception. - pub fn full_message(self) -> Option { + pub fn full_message(&self) -> Option { unsafe { if let Some(mut message) = rb_funcall_typed!(self.value, "full_message", [], RUBY_T_STRING) @@ -42,7 +42,7 @@ impl RubyException { } /// Get the backtrace string of the Ruby exception. - pub fn backtrace(self) -> Option { + pub fn backtrace(&self) -> Option { unsafe { if let Some(backtrace) = rb_funcall_typed!(self.value, "backtrace", [], RUBY_T_ARRAY) { let mut backtrace = rb_ary_join(backtrace, rb_str_new("\n".as_ptr() as _, 1)); @@ -60,7 +60,7 @@ impl RubyException { } /// Get the inspect string of the Ruby exception. - pub fn inspect(self) -> String { + pub fn inspect(&self) -> String { unsafe { if let Some(mut inspect) = rb_funcall_typed!(self.value, "inspect", [], RUBY_T_STRING) { rstring_to_string!(inspect) @@ -71,7 +71,7 @@ impl RubyException { } /// Get the class name of the Ruby exception. - pub fn classname(self) -> String { + pub fn classname(&self) -> String { unsafe { let classname = rb_class2name(rb_obj_class(self.value)); CStr::from_ptr(classname).to_string_lossy().into_owned() @@ -79,6 +79,12 @@ impl RubyException { } } +// impl Drop for RubyException { +// fn drop(&mut self) { +// rb_sys::rb_gc_guard!(self.value); +// } +// } + impl std::fmt::Debug for RubyException { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let message = self.message(); @@ -113,7 +119,7 @@ mod tests { use rb_sys::rb_eval_string; #[test] - fn test_exception() { + fn test_exception() -> Result<(), Box> { with_ruby_vm(|| { let exception = protect(|| unsafe { rb_eval_string("raise 'oh no'\0".as_ptr() as _); 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 74888529..ee718bb7 100644 --- a/crates/rb-sys-test-helpers/src/ruby_test_executor.rs +++ b/crates/rb-sys-test-helpers/src/ruby_test_executor.rs @@ -1,3 +1,4 @@ +use std::error::Error; use std::panic; use std::sync::mpsc::{self, SyncSender}; use std::sync::Once; @@ -15,34 +16,46 @@ use rb_sys::{ static mut GLOBAL_EXECUTOR: OnceCell = OnceCell::new(); pub struct RubyTestExecutor { - sender: Option>>, - handle: Option>, + #[allow(clippy::type_complexity)] + sender: Option Result<(), Box> + Send>>>, + handle: Option>>>, timeout: Duration, } impl RubyTestExecutor { pub fn start() -> Self { - let (sender, receiver) = mpsc::sync_channel::>(0); + let (sender, receiver) = + mpsc::sync_channel:: Result<(), Box> + Send>>(0); - let handle = thread::spawn(move || { + let handle = thread::spawn(move || -> Result<(), Box> { for closure in receiver { - closure(); + match closure() { + Ok(()) => {} + Err(err) => { + // transmute to avoid the Send bound + let err: Box = unsafe { std::mem::transmute(err) }; + return Err(err); + } + } } + Ok(()) }); let executor = Self { sender: Some(sender), handle: Some(handle), - timeout: Duration::from_secs(5), + timeout: Duration::from_secs(10), }; - executor.run(|| { - static INIT: Once = Once::new(); + executor + .run(|| { + static INIT: Once = Once::new(); - INIT.call_once(|| unsafe { - setup_ruby_unguarded(); - }); - }); + INIT.call_once(|| unsafe { + setup_ruby_unguarded(); + }) + }) + .expect("Failed to setup Ruby"); executor } @@ -54,7 +67,7 @@ impl RubyTestExecutor { pub fn shutdown(&mut self) { self.set_test_timeout(Duration::from_secs(3)); - self.run(|| unsafe { + let _ = self.run(|| unsafe { cleanup_ruby(); }); @@ -63,43 +76,36 @@ impl RubyTestExecutor { } if let Some(handle) = self.handle.take() { - handle.join().expect("Failed to join executor thread"); + let _ = handle.join().expect("Failed to join executor thread"); } } - pub fn run(&self, f: F) -> R + pub fn run(&self, f: F) -> Result> where F: FnOnce() -> R + Send + 'static, R: Send + 'static, { let (result_sender, result_receiver) = mpsc::sync_channel(1); - let closure = Box::new(move || { + let closure = Box::new(move || -> Result<(), Box> { let result = panic::catch_unwind(panic::AssertUnwindSafe(f)); - result_sender - .send(result) - .expect("Failed to send Ruby test result to Rust test thread"); + result_sender.send(result).map_err(Into::into) }); if let Some(sender) = self.sender.as_ref() { - sender - .send(closure) - .expect("Failed to send closure to Ruby test thread"); + sender.send(closure)?; } else { - panic!("RubyTestExecutor is not running"); + return Err("Ruby test executor is shutdown".into()); } match result_receiver.recv_timeout(self.timeout) { - Ok(Ok(result)) => result, + Ok(Ok(result)) => Ok(result), Ok(Err(err)) => std::panic::resume_unwind(err), - Err(_err) => { - eprintln!("Ruby test timed out after {:?}", self.timeout); - std::process::abort(); - } + Err(_err) => Err(format!("Ruby test timed out after {:?}", self.timeout).into()), } } - pub fn run_test(&self, f: F) -> R + pub fn run_test(&self, f: F) -> Result> where F: FnOnce() -> R + Send + 'static, R: Send + 'static, @@ -232,7 +238,7 @@ mod tests { executor.run_test(|| { unsafe { ruby_vm_at_exit(Some(set_called))} - }); + }).unwrap(); drop(executor); @@ -241,16 +247,17 @@ mod tests { } rusty_fork_test! { - // Flaky for some reason... - // #[test] - #[should_panic] + #[test] fn test_timeout() { let mut executor = RubyTestExecutor::start(); - executor.set_test_timeout(Duration::from_millis(1)); + executor.set_test_timeout(Duration::from_millis(10)); - executor.run_test(|| { - std::thread::sleep(Duration::from_millis(100)); - }); + let result = executor + .run_test(|| { + std::thread::sleep(Duration::from_millis(1000)); + }); + + assert_eq!("Ruby test timed out after 10ms", format!("{}", result.unwrap_err())); } } } diff --git a/crates/rb-sys-tests/src/lib.rs b/crates/rb-sys-tests/src/lib.rs index 753c55c3..8b823e5e 100644 --- a/crates/rb-sys-tests/src/lib.rs +++ b/crates/rb-sys-tests/src/lib.rs @@ -20,5 +20,8 @@ mod special_consts_test; #[cfg(test)] mod tracking_allocator_test; +#[cfg(all(test, unix))] +mod memory_test; + #[cfg(test)] mod stable_api_test; diff --git a/crates/rb-sys-tests/src/memory_test.rs b/crates/rb-sys-tests/src/memory_test.rs new file mode 100644 index 00000000..6bec95ee --- /dev/null +++ b/crates/rb-sys-tests/src/memory_test.rs @@ -0,0 +1,56 @@ +use rb_sys::VALUE; +use rb_sys::{rb_gc_guard, rb_str_cat_cstr, rb_str_new_cstr, RSTRING_PTR}; +use rb_sys_test_helpers::{rstring_to_string, ruby_test}; + +#[ruby_test(gc_stress)] +fn test_rb_gc_guarded_ptr_basic() { + unsafe { + let s = rb_str_new_cstr(" world\0".as_ptr() as _); + let sptr = RSTRING_PTR(s); + let t = rb_str_new_cstr("hello,\0".as_ptr() as _); + let mut string = rb_str_cat_cstr(t, sptr); + let result = rstring_to_string!(string); + + rb_gc_guard!(s); + rb_gc_guard!(t); + rb_gc_guard!(string); + + assert_eq!("hello, world", result); + } +} + +#[ruby_test(gc_stress)] +fn test_rb_gc_guarded_ptr_vec() { + for i in 0..42 { + unsafe { + let mut vec_of_values: Vec = Default::default(); + + let s1 = rb_str_new_cstr(format!("hello world{i}\0").as_ptr() as _); + vec_of_values.push(s1); + + let s2 = rb_str_new_cstr(format!("hello world{i}\0").as_ptr() as _); + vec_of_values.push(s2); + + let s3 = rb_str_new_cstr(format!("hello world{i}\0").as_ptr() as _); + vec_of_values.push(s3); + + let ptr = &vec_of_values.as_ptr(); + let len = &vec_of_values.len(); + + let rarray = rb_sys::rb_ary_new_from_values(*len as _, *ptr); + let mut inspected = rb_sys::rb_inspect(rarray); + let result = rstring_to_string!(inspected); + + rb_gc_guard!(s1); + rb_gc_guard!(s2); + rb_gc_guard!(s3); + rb_gc_guard!(rarray); + rb_gc_guard!(inspected); + + assert_eq!( + result, + format!("[\"hello world{i}\", \"hello world{i}\", \"hello world{i}\"]") + ); + } + } +} diff --git a/crates/rb-sys-tests/src/tracking_allocator_test.rs b/crates/rb-sys-tests/src/tracking_allocator_test.rs index a249d253..2debc836 100644 --- a/crates/rb-sys-tests/src/tracking_allocator_test.rs +++ b/crates/rb-sys-tests/src/tracking_allocator_test.rs @@ -180,6 +180,6 @@ rusty_fork_test! { with_ruby_vm(|| { assert_ne!(0, unsafe { rb_sys::rb_cObject }); - }); + }).unwrap(); } } diff --git a/crates/rb-sys/src/lib.rs b/crates/rb-sys/src/lib.rs index c3c41325..a69ca188 100644 --- a/crates/rb-sys/src/lib.rs +++ b/crates/rb-sys/src/lib.rs @@ -4,6 +4,7 @@ pub mod bindings; #[cfg(feature = "stable-api")] pub mod macros; +pub mod memory; pub mod special_consts; #[cfg(feature = "stable-api")] pub mod stable_api; @@ -17,6 +18,7 @@ mod utils; pub use bindings::*; #[cfg(feature = "stable-api")] pub use macros::*; +pub use memory::*; pub use ruby_abi_version::*; pub use special_consts::*; #[cfg(feature = "stable-api")] diff --git a/crates/rb-sys/src/memory.rs b/crates/rb-sys/src/memory.rs new file mode 100644 index 00000000..d2f672d6 --- /dev/null +++ b/crates/rb-sys/src/memory.rs @@ -0,0 +1,53 @@ +/// Prevents premature destruction of local objects. +/// +/// Ruby's garbage collector is conservative; it scans the C level machine stack as well. +/// Possible in-use Ruby objects must remain visible on stack, to be properly marked as such. +/// However, Rust's compiler optimizations might remove the references to these objects from +/// the stack when they are not being used directly. +/// +/// Consider the following example: +/// +/// ```ignore +/// use rb_sys::{rb_str_new_cstr, rb_str_cat_cstr, RSTRING_PTR, rb_gc_guard}; +/// +/// unsafe { +/// let s = rb_str_new_cstr(" world\0".as_ptr() as _); +/// let sptr = RSTRING_PTR(s); +/// let t = rb_str_new_cstr("hello,\0".as_ptr() as _); // Possible GC invocation +/// let u = rb_str_cat_cstr(t, sptr); +/// rb_gc_guard!(s); // ensure `s` (and thus `sptr`) do not get GC-ed +/// } +/// ``` +/// +/// In this example, without the `rb_gc_guard!`, the last use of `s` is before the last use +/// of `sptr`. Compilers could think `s` and `t` are allowed to overlap. That would +/// eliminate `s` from the stack, while `sptr` is still in use. If our GC runs at that +/// very moment, `s` gets swept out, which also destroys `sptr`. +/// +/// In order to prevent this scenario, `rb_gc_guard!` must be placed after the last use +/// of `sptr`. Placing `rb_gc_guard!` before dereferencing `sptr` would be of no use. +/// +/// Using the `rb_gc_guard!` macro has the following advantages: +/// +/// - the intent of the macro use is clear. +/// +/// - `rb_gc_guard!` only affects its call site, without negatively affecting other systems. +/// +/// # Example +/// ```no_run +/// use rb_sys::{rb_utf8_str_new_cstr, rb_gc_guard}; +/// +/// let my_string = unsafe { rb_utf8_str_new_cstr("hello world\0".as_ptr() as _) }; +/// let _ = rb_gc_guard!(my_string); +/// ``` +#[macro_export] +macro_rules! rb_gc_guard { + ($v:expr) => {{ + unsafe { + let val: $crate::VALUE = $v; + let rb_gc_guarded_ptr = std::ptr::read_volatile(&&val); + std::arch::asm!("/* {0} */", in(reg) rb_gc_guarded_ptr); + *rb_gc_guarded_ptr + } + }}; +} diff --git a/crates/rb-sys/src/stable_api.rs b/crates/rb-sys/src/stable_api.rs index 045e6608..aef1dca0 100644 --- a/crates/rb-sys/src/stable_api.rs +++ b/crates/rb-sys/src/stable_api.rs @@ -150,7 +150,7 @@ pub const fn get_default() -> &'static api::Definition { /// Get the fallback stable API definition for the current Ruby version, which /// is compiled C code that is linked into to this crate. -#[cfg(all(stable_api_enable_compiled_mod))] +#[cfg(stable_api_enable_compiled_mod)] pub const fn get_compiled() -> &'static compiled::Definition { const COMPILED_API: compiled::Definition = compiled::Definition {}; &COMPILED_API diff --git a/readme.md b/readme.md index cdb38d38..c6f113bc 100644 --- a/readme.md +++ b/readme.md @@ -58,14 +58,14 @@ directory for automation purposes): | Platform | Supported | Docker Image | | ----------------- | --------- | ---------------------------------------------- | -| x86_64-linux | โœ… | [`rbsys/x86_64-linux:0.9.79`][docker-hub] | -| x86_64-linux-musl | โœ… | [`rbsys/x86_64-linux-musl:0.9.79`][docker-hub] | -| aarch64-linux | โœ… | [`rbsys/aarch64-linux:0.9.79`][docker-hub] | -| arm-linux | โœ… | [`rbsys/arm-linux:0.9.79`][docker-hub] | -| arm64-darwin | โœ… | [`rbsys/arm64-darwin:0.9.79`][docker-hub] | -| x64-mingw32 | โœ… | [`rbsys/x64-mingw32:0.9.79`][docker-hub] | -| x64-mingw-ucrt | โœ… | [`rbsys/x64-mingw-ucrt:0.9.79`][docker-hub] | -| mswin | โœ… | not available on Docker | +| x86_64-linux | โœ… | [`rbsys/x86_64-linux:0.9.79`][docker-hub] | +| x86_64-linux-musl | โœ… | [`rbsys/x86_64-linux-musl:0.9.79`][docker-hub] | +| aarch64-linux | โœ… | [`rbsys/aarch64-linux:0.9.79`][docker-hub] | +| arm-linux | โœ… | [`rbsys/arm-linux:0.9.79`][docker-hub] | +| arm64-darwin | โœ… | [`rbsys/arm64-darwin:0.9.79`][docker-hub] | +| x64-mingw32 | โœ… | [`rbsys/x64-mingw32:0.9.79`][docker-hub] | +| x64-mingw-ucrt | โœ… | [`rbsys/x64-mingw-ucrt:0.9.79`][docker-hub] | +| mswin | โœ… | not available on Docker | ## Getting Help