diff options
Diffstat (limited to 'third_party/rust/uniffi_bindgen/src/bindings/ruby')
16 files changed, 1786 insertions, 0 deletions
diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs b/third_party/rust/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs new file mode 100644 index 0000000000..1f1bf8e299 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/gen_ruby/mod.rs @@ -0,0 +1,375 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use anyhow::Result; +use askama::Template; +use heck::{ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase}; +use serde::{Deserialize, Serialize}; +use std::borrow::Borrow; +use std::collections::HashMap; + +use crate::interface::*; +use crate::BindingsConfig; + +const RESERVED_WORDS: &[&str] = &[ + "alias", "and", "BEGIN", "begin", "break", "case", "class", "def", "defined?", "do", "else", + "elsif", "END", "end", "ensure", "false", "for", "if", "module", "next", "nil", "not", "or", + "redo", "rescue", "retry", "return", "self", "super", "then", "true", "undef", "unless", + "until", "when", "while", "yield", "__FILE__", "__LINE__", +]; + +fn is_reserved_word(word: &str) -> bool { + RESERVED_WORDS.contains(&word) +} + +/// Get the canonical, unique-within-this-component name for a type. +/// +/// When generating helper code for foreign language bindings, it's sometimes useful to be +/// able to name a particular type in order to e.g. call a helper function that is specific +/// to that type. We support this by defining a naming convention where each type gets a +/// unique canonical name, constructed recursively from the names of its component types (if any). +pub fn canonical_name(t: &Type) -> String { + match t { + // Builtin primitive types, with plain old names. + Type::Int8 => "i8".into(), + Type::UInt8 => "u8".into(), + Type::Int16 => "i16".into(), + Type::UInt16 => "u16".into(), + Type::Int32 => "i32".into(), + Type::UInt32 => "u32".into(), + Type::Int64 => "i64".into(), + Type::UInt64 => "u64".into(), + Type::Float32 => "f32".into(), + Type::Float64 => "f64".into(), + Type::String => "string".into(), + Type::Bytes => "bytes".into(), + Type::Boolean => "bool".into(), + // API defined types. + // Note that these all get unique names, and the parser ensures that the names do not + // conflict with a builtin type. We add a prefix to the name to guard against pathological + // cases like a record named `SequenceRecord` interfering with `sequence<Record>`. + // However, types that support importing all end up with the same prefix of "Type", so + // that the import handling code knows how to find the remote reference. + Type::Object { name, .. } => format!("Type{name}"), + Type::Enum { name, .. } => format!("Type{name}"), + Type::Record { name, .. } => format!("Type{name}"), + Type::CallbackInterface { name, .. } => format!("CallbackInterface{name}"), + Type::Timestamp => "Timestamp".into(), + Type::Duration => "Duration".into(), + Type::ForeignExecutor => "ForeignExecutor".into(), + // Recursive types. + // These add a prefix to the name of the underlying type. + // The component API definition cannot give names to recursive types, so as long as the + // prefixes we add here are all unique amongst themselves, then we have no chance of + // acccidentally generating name collisions. + Type::Optional { inner_type } => format!("Optional{}", canonical_name(inner_type)), + Type::Sequence { inner_type } => format!("Sequence{}", canonical_name(inner_type)), + Type::Map { + key_type, + value_type, + } => format!( + "Map{}{}", + canonical_name(key_type).to_upper_camel_case(), + canonical_name(value_type).to_upper_camel_case() + ), + // A type that exists externally. + Type::External { name, .. } | Type::Custom { name, .. } => format!("Type{name}"), + } +} + +// Some config options for it the caller wants to customize the generated ruby. +// Note that this can only be used to control details of the ruby *that do not affect the underlying component*, +// since the details of the underlying component are entirely determined by the `ComponentInterface`. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Config { + cdylib_name: Option<String>, + cdylib_path: Option<String>, +} + +impl Config { + pub fn cdylib_name(&self) -> String { + self.cdylib_name + .clone() + .unwrap_or_else(|| "uniffi".to_string()) + } + + pub fn custom_cdylib_path(&self) -> bool { + self.cdylib_path.is_some() + } + + pub fn cdylib_path(&self) -> String { + self.cdylib_path.clone().unwrap_or_default() + } +} + +impl BindingsConfig for Config { + fn update_from_ci(&mut self, ci: &ComponentInterface) { + self.cdylib_name + .get_or_insert_with(|| format!("uniffi_{}", ci.namespace())); + } + + fn update_from_cdylib_name(&mut self, cdylib_name: &str) { + self.cdylib_name + .get_or_insert_with(|| cdylib_name.to_string()); + } + + fn update_from_dependency_configs(&mut self, _config_map: HashMap<&str, &Self>) {} +} + +#[derive(Template)] +#[template(syntax = "rb", escape = "none", path = "wrapper.rb")] +pub struct RubyWrapper<'a> { + config: Config, + ci: &'a ComponentInterface, + canonical_name: &'a dyn Fn(&Type) -> String, +} +impl<'a> RubyWrapper<'a> { + pub fn new(config: Config, ci: &'a ComponentInterface) -> Self { + Self { + config, + ci, + canonical_name: &canonical_name, + } + } +} + +mod filters { + use super::*; + pub use crate::backend::filters::*; + + pub fn type_ffi(type_: &FfiType) -> Result<String, askama::Error> { + Ok(match type_ { + FfiType::Int8 => ":int8".to_string(), + FfiType::UInt8 => ":uint8".to_string(), + FfiType::Int16 => ":int16".to_string(), + FfiType::UInt16 => ":uint16".to_string(), + FfiType::Int32 => ":int32".to_string(), + FfiType::UInt32 => ":uint32".to_string(), + FfiType::Int64 => ":int64".to_string(), + FfiType::UInt64 => ":uint64".to_string(), + FfiType::Float32 => ":float".to_string(), + FfiType::Float64 => ":double".to_string(), + FfiType::RustArcPtr(_) => ":pointer".to_string(), + FfiType::RustBuffer(_) => "RustBuffer.by_value".to_string(), + FfiType::ForeignBytes => "ForeignBytes".to_string(), + FfiType::ForeignCallback => unimplemented!("Callback interfaces are not implemented"), + FfiType::ForeignExecutorCallback => { + unimplemented!("Foreign executors are not implemented") + } + FfiType::ForeignExecutorHandle => { + unimplemented!("Foreign executors are not implemented") + } + FfiType::RustFutureHandle + | FfiType::RustFutureContinuationCallback + | FfiType::RustFutureContinuationData => { + unimplemented!("Async functions are not implemented") + } + }) + } + + pub fn literal_rb(literal: &Literal) -> Result<String, askama::Error> { + Ok(match literal { + Literal::Boolean(v) => { + if *v { + "true".into() + } else { + "false".into() + } + } + // use the double-quote form to match with the other languages, and quote escapes. + Literal::String(s) => format!("\"{s}\""), + Literal::Null => "nil".into(), + Literal::EmptySequence => "[]".into(), + Literal::EmptyMap => "{}".into(), + Literal::Enum(v, type_) => match type_ { + Type::Enum { name, .. } => { + format!("{}::{}", class_name_rb(name)?, enum_name_rb(v)?) + } + _ => panic!("Unexpected type in enum literal: {type_:?}"), + }, + // https://docs.ruby-lang.org/en/2.0.0/syntax/literals_rdoc.html + Literal::Int(i, radix, _) => match radix { + Radix::Octal => format!("0o{i:o}"), + Radix::Decimal => format!("{i}"), + Radix::Hexadecimal => format!("{i:#x}"), + }, + Literal::UInt(i, radix, _) => match radix { + Radix::Octal => format!("0o{i:o}"), + Radix::Decimal => format!("{i}"), + Radix::Hexadecimal => format!("{i:#x}"), + }, + Literal::Float(string, _type_) => string.clone(), + }) + } + + pub fn class_name_rb(nm: &str) -> Result<String, askama::Error> { + Ok(nm.to_string().to_upper_camel_case()) + } + + pub fn fn_name_rb(nm: &str) -> Result<String, askama::Error> { + Ok(nm.to_string().to_snake_case()) + } + + pub fn var_name_rb(nm: &str) -> Result<String, askama::Error> { + let nm = nm.to_string(); + let prefix = if is_reserved_word(&nm) { "_" } else { "" }; + + Ok(format!("{prefix}{}", nm.to_snake_case())) + } + + pub fn enum_name_rb(nm: &str) -> Result<String, askama::Error> { + Ok(nm.to_string().to_shouty_snake_case()) + } + + pub fn coerce_rb(nm: &str, ns: &str, type_: &Type) -> Result<String, askama::Error> { + Ok(match type_ { + Type::Int8 => format!("{ns}::uniffi_in_range({nm}, \"i8\", -2**7, 2**7)"), + Type::Int16 => format!("{ns}::uniffi_in_range({nm}, \"i16\", -2**15, 2**15)"), + Type::Int32 => format!("{ns}::uniffi_in_range({nm}, \"i32\", -2**31, 2**31)"), + Type::Int64 => format!("{ns}::uniffi_in_range({nm}, \"i64\", -2**63, 2**63)"), + Type::UInt8 => format!("{ns}::uniffi_in_range({nm}, \"u8\", 0, 2**8)"), + Type::UInt16 => format!("{ns}::uniffi_in_range({nm}, \"u16\", 0, 2**16)"), + Type::UInt32 => format!("{ns}::uniffi_in_range({nm}, \"u32\", 0, 2**32)"), + Type::UInt64 => format!("{ns}::uniffi_in_range({nm}, \"u64\", 0, 2**64)"), + Type::Float32 | Type::Float64 => nm.to_string(), + Type::Boolean => format!("{nm} ? true : false"), + Type::Object { .. } | Type::Enum { .. } | Type::Record { .. } => nm.to_string(), + Type::String => format!("{ns}::uniffi_utf8({nm})"), + Type::Bytes => format!("{ns}::uniffi_bytes({nm})"), + Type::Timestamp | Type::Duration => nm.to_string(), + Type::CallbackInterface { .. } => { + panic!("No support for coercing callback interfaces yet") + } + Type::Optional { inner_type: t } => format!("({nm} ? {} : nil)", coerce_rb(nm, ns, t)?), + Type::Sequence { inner_type: t } => { + let coerce_code = coerce_rb("v", ns, t)?; + if coerce_code == "v" { + nm.to_string() + } else { + format!("{nm}.map {{ |v| {coerce_code} }}") + } + } + Type::Map { value_type: t, .. } => { + let k_coerce_code = coerce_rb("k", ns, &Type::String)?; + let v_coerce_code = coerce_rb("v", ns, t)?; + + if k_coerce_code == "k" && v_coerce_code == "v" { + nm.to_string() + } else { + format!( + "{nm}.each.with_object({{}}) {{ |(k, v), res| res[{k_coerce_code}] = {v_coerce_code} }}" + ) + } + } + Type::External { .. } => panic!("No support for external types, yet"), + Type::Custom { .. } => panic!("No support for custom types, yet"), + Type::ForeignExecutor => unimplemented!("Foreign executors are not implemented"), + }) + } + + pub fn lower_rb(nm: &str, type_: &Type) -> Result<String, askama::Error> { + Ok(match type_ { + Type::Int8 + | Type::UInt8 + | Type::Int16 + | Type::UInt16 + | Type::Int32 + | Type::UInt32 + | Type::Int64 + | Type::UInt64 + | Type::Float32 + | Type::Float64 => nm.to_string(), + Type::Boolean => format!("({nm} ? 1 : 0)"), + Type::String => format!("RustBuffer.allocFromString({nm})"), + Type::Bytes => format!("RustBuffer.allocFromBytes({nm})"), + Type::Object { name, .. } => format!("({}._uniffi_lower {nm})", class_name_rb(name)?), + Type::CallbackInterface { .. } => { + panic!("No support for lowering callback interfaces yet") + } + Type::Enum { .. } + | Type::Record { .. } + | Type::Optional { .. } + | Type::Sequence { .. } + | Type::Timestamp + | Type::Duration + | Type::Map { .. } => format!( + "RustBuffer.alloc_from_{}({})", + class_name_rb(&canonical_name(type_))?, + nm + ), + Type::External { .. } => panic!("No support for lowering external types, yet"), + Type::Custom { .. } => panic!("No support for lowering custom types, yet"), + Type::ForeignExecutor => unimplemented!("Foreign executors are not implemented"), + }) + } + + pub fn lift_rb(nm: &str, type_: &Type) -> Result<String, askama::Error> { + Ok(match type_ { + Type::Int8 + | Type::UInt8 + | Type::Int16 + | Type::UInt16 + | Type::Int32 + | Type::UInt32 + | Type::Int64 + | Type::UInt64 => format!("{nm}.to_i"), + Type::Float32 | Type::Float64 => format!("{nm}.to_f"), + Type::Boolean => format!("1 == {nm}"), + Type::String => format!("{nm}.consumeIntoString"), + Type::Bytes => format!("{nm}.consumeIntoBytes"), + Type::Object { name, .. } => format!("{}._uniffi_allocate({nm})", class_name_rb(name)?), + Type::CallbackInterface { .. } => { + panic!("No support for lifting callback interfaces, yet") + } + Type::Enum { .. } => { + format!( + "{}.consumeInto{}", + nm, + class_name_rb(&canonical_name(type_))? + ) + } + Type::Record { .. } + | Type::Optional { .. } + | Type::Sequence { .. } + | Type::Timestamp + | Type::Duration + | Type::Map { .. } => format!( + "{}.consumeInto{}", + nm, + class_name_rb(&canonical_name(type_))? + ), + Type::External { .. } => panic!("No support for lifting external types, yet"), + Type::Custom { .. } => panic!("No support for lifting custom types, yet"), + Type::ForeignExecutor => unimplemented!("Foreign executors are not implemented"), + }) + } +} + +#[cfg(test)] +mod test_type { + use super::*; + + #[test] + fn test_canonical_names() { + // Non-exhaustive, but gives a bit of a flavour of what we want. + assert_eq!(canonical_name(&Type::UInt8), "u8"); + assert_eq!(canonical_name(&Type::String), "string"); + assert_eq!(canonical_name(&Type::Bytes), "bytes"); + assert_eq!( + canonical_name(&Type::Optional { + inner_type: Box::new(Type::Sequence { + inner_type: Box::new(Type::Object { + module_path: "anything".to_string(), + name: "Example".into(), + imp: ObjectImpl::Struct, + }) + }) + }), + "OptionalSequenceTypeExample" + ); + } +} + +#[cfg(test)] +mod tests; diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/gen_ruby/tests.rs b/third_party/rust/uniffi_bindgen/src/bindings/ruby/gen_ruby/tests.rs new file mode 100644 index 0000000000..9ae5d1816f --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/gen_ruby/tests.rs @@ -0,0 +1,47 @@ +use super::{is_reserved_word, Config}; + +#[test] +fn when_reserved_word() { + assert!(is_reserved_word("end")); +} + +#[test] +fn when_not_reserved_word() { + assert!(!is_reserved_word("ruby")); +} + +#[test] +fn cdylib_name() { + let config = Config { + cdylib_name: None, + cdylib_path: None, + }; + + assert_eq!("uniffi", config.cdylib_name()); + + let config = Config { + cdylib_name: Some("todolist".to_string()), + cdylib_path: None, + }; + + assert_eq!("todolist", config.cdylib_name()); +} + +#[test] +fn cdylib_path() { + let config = Config { + cdylib_name: None, + cdylib_path: None, + }; + + assert_eq!("", config.cdylib_path()); + assert!(!config.custom_cdylib_path()); + + let config = Config { + cdylib_name: None, + cdylib_path: Some("/foo/bar".to_string()), + }; + + assert_eq!("/foo/bar", config.cdylib_path()); + assert!(config.custom_cdylib_path()); +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/mod.rs b/third_party/rust/uniffi_bindgen/src/bindings/ruby/mod.rs new file mode 100644 index 0000000000..6db89b9f0b --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/mod.rs @@ -0,0 +1,48 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::process::Command; + +use anyhow::{Context, Result}; +use camino::Utf8Path; +use fs_err as fs; + +pub mod gen_ruby; +mod test; +pub use gen_ruby::{Config, RubyWrapper}; +pub use test::{run_test, test_script_command}; + +use super::super::interface::ComponentInterface; + +// Generate ruby bindings for the given ComponentInterface, in the given output directory. + +pub fn write_bindings( + config: &Config, + ci: &ComponentInterface, + out_dir: &Utf8Path, + try_format_code: bool, +) -> Result<()> { + let rb_file = out_dir.join(format!("{}.rb", ci.namespace())); + fs::write(&rb_file, generate_ruby_bindings(config, ci)?)?; + + if try_format_code { + if let Err(e) = Command::new("rubocop").arg("-A").arg(&rb_file).output() { + println!( + "Warning: Unable to auto-format {} using rubocop: {e:?}", + rb_file.file_name().unwrap(), + ) + } + } + + Ok(()) +} + +// Generate ruby bindings for the given ComponentInterface, as a string. + +pub fn generate_ruby_bindings(config: &Config, ci: &ComponentInterface) -> Result<String> { + use askama::Template; + RubyWrapper::new(config.clone(), ci) + .render() + .context("failed to render ruby bindings") +} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/EnumTemplate.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/EnumTemplate.rb new file mode 100644 index 0000000000..23b701f6a7 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/EnumTemplate.rb @@ -0,0 +1,59 @@ +{% if e.is_flat() %} + +class {{ e.name()|class_name_rb }} + {% for variant in e.variants() -%} + {{ variant.name()|enum_name_rb }} = {{ loop.index }} + {% endfor %} +end + +{% else %} + +class {{ e.name()|class_name_rb }} + def initialize + raise RuntimeError, '{{ e.name()|class_name_rb }} cannot be instantiated directly' + end + + # Each enum variant is a nested class of the enum itself. + {% for variant in e.variants() -%} + class {{ variant.name()|enum_name_rb }} + {% if variant.has_fields() %} + attr_reader {% for field in variant.fields() %}:{{ field.name()|var_name_rb }}{% if loop.last %}{% else %}, {% endif %}{%- endfor %} + {% endif %} + def initialize({% for field in variant.fields() %}{{ field.name()|var_name_rb }}{% if loop.last %}{% else %}, {% endif %}{% endfor %}) + {% if variant.has_fields() %} + {%- for field in variant.fields() %} + @{{ field.name()|var_name_rb }} = {{ field.name()|var_name_rb }} + {%- endfor %} + {% else %} + {% endif %} + end + + def to_s + "{{ e.name()|class_name_rb }}::{{ variant.name()|enum_name_rb }}({% for field in variant.fields() %}{{ field.name() }}=#{@{{ field.name() }}}{% if loop.last %}{% else %}, {% endif %}{% endfor %})" + end + + def ==(other) + if !other.{{ variant.name()|var_name_rb }}? + return false + end + {%- for field in variant.fields() %} + if @{{ field.name()|var_name_rb }} != other.{{ field.name()|var_name_rb }} + return false + end + {%- endfor %} + + true + end + + # For each variant, we have an `NAME?` method for easily checking + # whether an instance is that variant. + {% for variant in e.variants() %} + def {{ variant.name()|var_name_rb }}? + instance_of? {{ e.name()|class_name_rb }}::{{ variant.name()|enum_name_rb }} + end + {% endfor %} + end + {% endfor %} +end + +{% endif %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/ErrorTemplate.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/ErrorTemplate.rb new file mode 100644 index 0000000000..a7e26370c8 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/ErrorTemplate.rb @@ -0,0 +1,121 @@ +class RustCallStatus < FFI::Struct + layout :code, :int8, + :error_buf, RustBuffer + + def code + self[:code] + end + + def error_buf + self[:error_buf] + end + + def to_s + "RustCallStatus(code=#{self[:code]})" + end +end + +# These match the values from the uniffi::rustcalls module +CALL_SUCCESS = 0 +CALL_ERROR = 1 +CALL_PANIC = 2 +{%- for e in ci.enum_definitions() %} +{% if ci.is_name_used_as_error(e.name()) %} +{% if e.is_flat() %} +class {{ e.name()|class_name_rb }} + {%- for variant in e.variants() %} + {{ variant.name()|class_name_rb }} = Class.new StandardError + {%- endfor %} +{% else %} +module {{ e.name()|class_name_rb }} + {%- for variant in e.variants() %} + class {{ variant.name()|class_name_rb }} < StandardError + def initialize({% for field in variant.fields() %}{{ field.name()|var_name_rb }}{% if !loop.last %}, {% endif %}{% endfor %}) + {%- for field in variant.fields() %} + @{{ field.name()|var_name_rb }} = {{ field.name()|var_name_rb }} + {%- endfor %} + super() + end + {%- if variant.has_fields() %} + + attr_reader {% for field in variant.fields() %}:{{ field.name()|var_name_rb }}{% if !loop.last %}, {% endif %}{% endfor %} + {% endif %} + + def to_s + "#{self.class.name}({% for field in variant.fields() %}{{ field.name()|var_name_rb }}=#{@{{ field.name()|var_name_rb }}.inspect}{% if !loop.last %}, {% endif %}{% endfor %})" + end + end + {%- endfor %} +{% endif %} +end +{% endif %} +{%- endfor %} + +# Map error modules to the RustBuffer method name that reads them +ERROR_MODULE_TO_READER_METHOD = { +{%- for e in ci.enum_definitions() %} +{% if ci.is_name_used_as_error(e.name()) %} +{%- let typ=ci.get_type(e.name()).unwrap() %} +{%- let canonical_type_name = canonical_name(typ.borrow()).borrow()|class_name_rb %} + {{ e.name()|class_name_rb }} => :read{{ canonical_type_name }}, +{% endif %} +{%- endfor %} +} + +private_constant :ERROR_MODULE_TO_READER_METHOD, :CALL_SUCCESS, :CALL_ERROR, :CALL_PANIC, + :RustCallStatus + +def self.consume_buffer_into_error(error_module, rust_buffer) + rust_buffer.consumeWithStream do |stream| + reader_method = ERROR_MODULE_TO_READER_METHOD[error_module] + return stream.send(reader_method) + end +end + +class InternalError < StandardError +end + +def self.rust_call(fn_name, *args) + # Call a rust function + rust_call_with_error(nil, fn_name, *args) +end + +def self.rust_call_with_error(error_module, fn_name, *args) + # Call a rust function and handle errors + # + # Use this when the rust function returns a Result<>. error_module must be the error_module that corresponds to that Result. + + + # Note: RustCallStatus.new zeroes out the struct, which is exactly what we + # want to pass to Rust (code=0, error_buf=RustBuffer(len=0, capacity=0, + # data=NULL)) + status = RustCallStatus.new + args << status + + result = UniFFILib.public_send(fn_name, *args) + + case status.code + when CALL_SUCCESS + result + when CALL_ERROR + if error_module.nil? + status.error_buf.free + raise InternalError, "CALL_ERROR with no error_module set" + else + raise consume_buffer_into_error(error_module, status.error_buf) + end + when CALL_PANIC + # When the rust code sees a panic, it tries to construct a RustBuffer + # with the message. But if that code panics, then it just sends back + # an empty buffer. + if status.error_buf.len > 0 + raise InternalError, status.error_buf.consumeIntoString() + else + raise InternalError, "Rust panic" + end + else + raise InternalError, "Unknown call status: #{status.code}" + end +end + +private_class_method :consume_buffer_into_error diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/Helpers.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/Helpers.rb new file mode 100644 index 0000000000..49fa247a77 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/Helpers.rb @@ -0,0 +1,18 @@ +def self.uniffi_in_range(i, type_name, min, max) + raise TypeError, "no implicit conversion of #{i} into Integer" unless i.respond_to?(:to_int) + i = i.to_int + raise RangeError, "#{type_name} requires #{min} <= value < #{max}" unless (min <= i && i < max) + i +end + +def self.uniffi_utf8(v) + raise TypeError, "no implicit conversion of #{v} into String" unless v.respond_to?(:to_str) + v = v.to_str.encode(Encoding::UTF_8) + raise Encoding::InvalidByteSequenceError, "not a valid UTF-8 encoded string" unless v.valid_encoding? + v +end + +def self.uniffi_bytes(v) + raise TypeError, "no implicit conversion of #{v} into String" unless v.respond_to?(:to_str) + v.to_str +end diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/NamespaceLibraryTemplate.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/NamespaceLibraryTemplate.rb new file mode 100644 index 0000000000..8536fc322e --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/NamespaceLibraryTemplate.rb @@ -0,0 +1,17 @@ +# This is how we find and load the dynamic library provided by the component. +# For now we just look it up by name. +module UniFFILib + extend FFI::Library + + {% if config.custom_cdylib_path() %} + ffi_lib {{ config.cdylib_path() }} + {% else %} + ffi_lib '{{ config.cdylib_name() }}' + {% endif %} + + {% for func in ci.iter_ffi_function_definitions_non_async() -%} + attach_function :{{ func.name() }}, + {%- call rb::arg_list_ffi_decl(func) %}, + {% match func.return_type() %}{% when Some with (type_) %}{{ type_|type_ffi }}{% when None %}:void{% endmatch %} + {% endfor %} +end diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/ObjectTemplate.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/ObjectTemplate.rb new file mode 100644 index 0000000000..677c5c729b --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/ObjectTemplate.rb @@ -0,0 +1,73 @@ +class {{ obj.name()|class_name_rb }} + + # A private helper for initializing instances of the class from a raw pointer, + # bypassing any initialization logic and ensuring they are GC'd properly. + def self._uniffi_allocate(pointer) + pointer.autorelease = false + inst = allocate + inst.instance_variable_set :@pointer, pointer + ObjectSpace.define_finalizer(inst, _uniffi_define_finalizer_by_pointer(pointer, inst.object_id)) + return inst + end + + # A private helper for registering an object finalizer. + # N.B. it's important that this does not capture a reference + # to the actual instance, only its underlying pointer. + def self._uniffi_define_finalizer_by_pointer(pointer, object_id) + Proc.new do |_id| + {{ ci.namespace()|class_name_rb }}.rust_call( + :{{ obj.ffi_object_free().name() }}, + pointer + ) + end + end + + # A private helper for lowering instances into a raw pointer. + # This does an explicit typecheck, because accidentally lowering a different type of + # object in a place where this type is expected, could lead to memory unsafety. + def self._uniffi_lower(inst) + if not inst.is_a? self + raise TypeError.new "Expected a {{ obj.name()|class_name_rb }} instance, got #{inst}" + end + return inst.instance_variable_get :@pointer + end + + {%- match obj.primary_constructor() %} + {%- when Some with (cons) %} + def initialize({% call rb::arg_list_decl(cons) -%}) + {%- call rb::coerce_args_extra_indent(cons) %} + pointer = {% call rb::to_ffi_call(cons) %} + @pointer = pointer + ObjectSpace.define_finalizer(self, self.class._uniffi_define_finalizer_by_pointer(pointer, self.object_id)) + end + {%- when None %} + {%- endmatch %} + + {% for cons in obj.alternate_constructors() -%} + def self.{{ cons.name()|fn_name_rb }}({% call rb::arg_list_decl(cons) %}) + {%- call rb::coerce_args_extra_indent(cons) %} + # Call the (fallible) function before creating any half-baked object instances. + # Lightly yucky way to bypass the usual "initialize" logic + # and just create a new instance with the required pointer. + return _uniffi_allocate({% call rb::to_ffi_call(cons) %}) + end + {% endfor %} + + {% for meth in obj.methods() -%} + {%- match meth.return_type() -%} + + {%- when Some with (return_type) -%} + def {{ meth.name()|fn_name_rb }}({% call rb::arg_list_decl(meth) %}) + {%- call rb::coerce_args_extra_indent(meth) %} + result = {% call rb::to_ffi_call_with_prefix("@pointer", meth) %} + return {{ "result"|lift_rb(return_type) }} + end + + {%- when None -%} + def {{ meth.name()|fn_name_rb }}({% call rb::arg_list_decl(meth) %}) + {%- call rb::coerce_args_extra_indent(meth) %} + {% call rb::to_ffi_call_with_prefix("@pointer", meth) %} + end + {% endmatch %} + {% endfor %} +end diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RecordTemplate.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RecordTemplate.rb new file mode 100644 index 0000000000..c940b31060 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RecordTemplate.rb @@ -0,0 +1,20 @@ +# Record type {{ rec.name() }} +class {{ rec.name()|class_name_rb }} + attr_reader {% for field in rec.fields() %}:{{ field.name()|var_name_rb }}{% if loop.last %}{% else %}, {% endif %}{%- endfor %} + + def initialize({% for field in rec.fields() %}{{ field.name()|var_name_rb }}{% if loop.last %}{% else %}, {% endif %}{% endfor %}) + {%- for field in rec.fields() %} + @{{ field.name()|var_name_rb }} = {{ field.name()|var_name_rb }} + {%- endfor %} + end + + def ==(other) + {%- for field in rec.fields() %} + if @{{ field.name()|var_name_rb }} != other.{{ field.name()|var_name_rb }} + return false + end + {%- endfor %} + + true + end +end diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RustBufferBuilder.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RustBufferBuilder.rb new file mode 100644 index 0000000000..8749139116 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RustBufferBuilder.rb @@ -0,0 +1,264 @@ + +# Helper for structured writing of values into a RustBuffer. +class RustBufferBuilder + def initialize + @rust_buf = RustBuffer.alloc 16 + @rust_buf.len = 0 + end + + def finalize + rbuf = @rust_buf + + @rust_buf = nil + + rbuf + end + + def discard + return if @rust_buf.nil? + + rbuf = finalize + rbuf.free + end + + def write(value) + reserve(value.bytes.size) do + @rust_buf.data.put_array_of_char @rust_buf.len, value.bytes + end + end + + {% for typ in ci.iter_types() -%} + {%- let canonical_type_name = canonical_name(typ).borrow()|class_name_rb -%} + {%- match typ -%} + + {% when Type::Int8 -%} + + def write_I8(v) + v = {{ ci.namespace()|class_name_rb }}::uniffi_in_range(v, "i8", -2**7, 2**7) + pack_into(1, 'c', v) + end + + {% when Type::UInt8 -%} + + def write_U8(v) + v = {{ ci.namespace()|class_name_rb }}::uniffi_in_range(v, "u8", 0, 2**8) + pack_into(1, 'c', v) + end + + {% when Type::Int16 -%} + + def write_I16(v) + v = {{ ci.namespace()|class_name_rb }}::uniffi_in_range(v, "i16", -2**15, 2**15) + pack_into(2, 's>', v) + end + + {% when Type::UInt16 -%} + + def write_U16(v) + v = {{ ci.namespace()|class_name_rb }}::uniffi_in_range(v, "u16", 0, 2**16) + pack_into(2, 'S>', v) + end + + {% when Type::Int32 -%} + + def write_I32(v) + v = {{ ci.namespace()|class_name_rb }}::uniffi_in_range(v, "i32", -2**31, 2**31) + pack_into(4, 'l>', v) + end + + {% when Type::UInt32 -%} + + def write_U32(v) + v = {{ ci.namespace()|class_name_rb }}::uniffi_in_range(v, "u32", 0, 2**32) + pack_into(4, 'L>', v) + end + + {% when Type::Int64 -%} + + def write_I64(v) + v = {{ ci.namespace()|class_name_rb }}::uniffi_in_range(v, "i64", -2**63, 2**63) + pack_into(8, 'q>', v) + end + + {% when Type::UInt64 -%} + + def write_U64(v) + v = {{ ci.namespace()|class_name_rb }}::uniffi_in_range(v, "u64", 0, 2**64) + pack_into(8, 'Q>', v) + end + + {% when Type::Float32 -%} + + def write_F32(v) + pack_into(4, 'g', v) + end + + {% when Type::Float64 -%} + + def write_F64(v) + pack_into(8, 'G', v) + end + + {% when Type::Boolean -%} + + def write_Bool(v) + pack_into(1, 'c', v ? 1 : 0) + end + + {% when Type::String -%} + + def write_String(v) + v = {{ ci.namespace()|class_name_rb }}::uniffi_utf8(v) + pack_into 4, 'l>', v.bytes.size + write v + end + + {% when Type::Bytes -%} + + def write_Bytes(v) + v = {{ ci.namespace()|class_name_rb }}::uniffi_bytes(v) + pack_into 4, 'l>', v.bytes.size + write v + end + + {% when Type::Timestamp -%} + # The Timestamp type. + ONE_SECOND_IN_NANOSECONDS = 10**9 + + def write_{{ canonical_type_name }}(v) + seconds = v.tv_sec + nanoseconds = v.tv_nsec + + # UniFFi conventions assume that nanoseconds part has to represent nanoseconds portion of + # duration between epoch and the timestamp moment. Ruby `Time#tv_nsec` returns the number of + # nanoseconds for the subsecond part, which is sort of opposite to "duration" meaning. + # Hence we need to convert value returned by `Time#tv_nsec` back and forth with the following + # logic: + if seconds < 0 && nanoseconds != 0 + # In order to get duration nsec we shift by 1 second: + nanoseconds = ONE_SECOND_IN_NANOSECONDS - nanoseconds + + # Then we compensate 1 second shift: + seconds += 1 + end + + pack_into 8, 'q>', seconds + pack_into 4, 'L>', nanoseconds + end + + {% when Type::Duration -%} + # The Duration type. + + def write_{{ canonical_type_name }}(v) + seconds = v.tv_sec + nanoseconds = v.tv_nsec + + raise ArgumentError, 'Invalid duration, must be non-negative' if seconds < 0 + + pack_into 8, 'Q>', seconds + pack_into 4, 'L>', nanoseconds + end + + {% when Type::Object with { name: object_name, module_path, imp } -%} + # The Object type {{ object_name }}. + + def write_{{ canonical_type_name }}(obj) + pointer = {{ object_name|class_name_rb}}._uniffi_lower obj + pack_into(8, 'Q>', pointer.address) + end + + {% when Type::Enum { name: enum_name, module_path } -%} + {% if !ci.is_name_used_as_error(enum_name) %} + {%- let e = ci|get_enum_definition(enum_name) -%} + # The Enum type {{ enum_name }}. + + def write_{{ canonical_type_name }}(v) + {%- if e.is_flat() %} + pack_into(4, 'l>', v) + {%- else -%} + {%- for variant in e.variants() %} + if v.{{ variant.name()|var_name_rb }}? + pack_into(4, 'l>', {{ loop.index }}) + {%- for field in variant.fields() %} + self.write_{{ canonical_name(field.as_type().borrow()).borrow()|class_name_rb }}(v.{{ field.name() }}) + {%- endfor %} + end + {%- endfor %} + {%- endif %} + end + {% endif %} + + {% when Type::Record { name: record_name, module_path } -%} + {%- let rec = ci|get_record_definition(record_name) -%} + # The Record type {{ record_name }}. + + def write_{{ canonical_type_name }}(v) + {%- for field in rec.fields() %} + self.write_{{ canonical_name(field.as_type().borrow()).borrow()|class_name_rb }}(v.{{ field.name()|var_name_rb }}) + {%- endfor %} + end + + {% when Type::Optional { inner_type } -%} + # The Optional<T> type for {{ canonical_name(inner_type) }}. + + def write_{{ canonical_type_name }}(v) + if v.nil? + pack_into(1, 'c', 0) + else + pack_into(1, 'c', 1) + self.write_{{ canonical_name(inner_type).borrow()|class_name_rb }}(v) + end + end + + {% when Type::Sequence { inner_type } -%} + # The Sequence<T> type for {{ canonical_name(inner_type) }}. + + def write_{{ canonical_type_name }}(items) + pack_into(4, 'l>', items.size) + + items.each do |item| + self.write_{{ canonical_name(inner_type).borrow()|class_name_rb }}(item) + end + end + + {% when Type::Map { key_type: k, value_type: inner_type } -%} + # The Map<T> type for {{ canonical_name(inner_type) }}. + + def write_{{ canonical_type_name }}(items) + pack_into(4, 'l>', items.size) + + items.each do |k, v| + write_String(k) + self.write_{{ canonical_name(inner_type).borrow()|class_name_rb }}(v) + end + end + + {%- else -%} + # This type is not yet supported in the Ruby backend. + def write_{{ canonical_type_name }}(v) + raise InternalError('RustBufferStream.write() not implemented yet for {{ canonical_type_name }}') + end + + {%- endmatch -%} + {%- endfor %} + + private + + def reserve(num_bytes) + if @rust_buf.len + num_bytes > @rust_buf.capacity + @rust_buf = RustBuffer.reserve(@rust_buf, num_bytes) + end + + yield + + @rust_buf.len += num_bytes + end + + def pack_into(size, format, value) + reserve(size) do + @rust_buf.data.put_array_of_char @rust_buf.len, [value].pack(format).bytes + end + end +end + +private_constant :RustBufferBuilder diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RustBufferStream.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RustBufferStream.rb new file mode 100644 index 0000000000..b085dddf15 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RustBufferStream.rb @@ -0,0 +1,315 @@ + +# Helper for structured reading of values from a RustBuffer. +class RustBufferStream + + def initialize(rbuf) + @rbuf = rbuf + @offset = 0 + end + + def remaining + @rbuf.len - @offset + end + + def read(size) + raise InternalError, 'read past end of rust buffer' if @offset + size > @rbuf.len + + data = @rbuf.data.get_bytes @offset, size + + @offset += size + + data + end + + {% for typ in ci.iter_types() -%} + {%- let canonical_type_name = canonical_name(typ).borrow()|class_name_rb -%} + {%- match typ -%} + + {% when Type::Int8 -%} + + def readI8 + unpack_from 1, 'c' + end + + {% when Type::UInt8 -%} + + def readU8 + unpack_from 1, 'c' + end + + {% when Type::Int16 -%} + + def readI16 + unpack_from 2, 's>' + end + + {% when Type::UInt16 -%} + + def readU16 + unpack_from 2, 'S>' + end + + {% when Type::Int32 -%} + + def readI32 + unpack_from 4, 'l>' + end + + {% when Type::UInt32 -%} + + def readU32 + unpack_from 4, 'L>' + end + + {% when Type::Int64 -%} + + def readI64 + unpack_from 8, 'q>' + end + + {% when Type::UInt64 -%} + + def readU64 + unpack_from 8, 'Q>' + end + + {% when Type::Float32 -%} + + def readF32 + unpack_from 4, 'g' + end + + {% when Type::Float64 -%} + + def readF64 + unpack_from 8, 'G' + end + + {% when Type::Boolean -%} + + def readBool + v = unpack_from 1, 'c' + + return false if v == 0 + return true if v == 1 + + raise InternalError, 'Unexpected byte for Boolean type' + end + + {% when Type::String -%} + + def readString + size = unpack_from 4, 'l>' + + raise InternalError, 'Unexpected negative string length' if size.negative? + + read(size).force_encoding(Encoding::UTF_8) + end + + {% when Type::Bytes -%} + + def readBytes + size = unpack_from 4, 'l>' + + raise InternalError, 'Unexpected negative byte string length' if size.negative? + + read(size).force_encoding(Encoding::BINARY) + end + + {% when Type::Timestamp -%} + # The Timestamp type. + ONE_SECOND_IN_NANOSECONDS = 10**9 + + def read{{ canonical_type_name }} + seconds = unpack_from 8, 'q>' + nanoseconds = unpack_from 4, 'L>' + + # UniFFi conventions assume that nanoseconds part has to represent nanoseconds portion of + # duration between epoch and the timestamp moment. Ruby `Time#tv_nsec` returns the number of + # nanoseconds for the subsecond part, which is sort of opposite to "duration" meaning. + # Hence we need to convert value returned by `Time#tv_nsec` back and forth with the following + # logic: + if seconds < 0 && nanoseconds != 0 + # In order to get duration nsec we shift by 1 second: + nanoseconds = ONE_SECOND_IN_NANOSECONDS - nanoseconds + + # Then we compensate 1 second shift: + seconds -= 1 + end + + Time.at(seconds, nanoseconds, :nanosecond, in: '+00:00').utc + end + + {% when Type::Duration -%} + # The Duration type. + + def read{{ canonical_type_name }} + seconds = unpack_from 8, 'q>' + nanoseconds = unpack_from 4, 'L>' + + Time.at(seconds, nanoseconds, :nanosecond, in: '+00:00').utc + end + + {% when Type::Object with { name: object_name, module_path, imp } -%} + # The Object type {{ object_name }}. + + def read{{ canonical_type_name }} + pointer = FFI::Pointer.new unpack_from 8, 'Q>' + return {{ object_name|class_name_rb }}._uniffi_allocate(pointer) + end + + {% when Type::Enum { name, module_path } -%} + {%- let e = ci|get_enum_definition(name) -%} + {% if !ci.is_name_used_as_error(name) %} + {% let enum_name = name %} + # The Enum type {{ enum_name }}. + + def read{{ canonical_type_name }} + variant = unpack_from 4, 'l>' + {% if e.is_flat() -%} + {%- for variant in e.variants() %} + if variant == {{ loop.index }} + return {{ enum_name|class_name_rb }}::{{ variant.name()|enum_name_rb }} + end + {%- endfor %} + + raise InternalError, 'Unexpected variant tag for {{ canonical_type_name }}' + {%- else -%} + {%- for variant in e.variants() %} + if variant == {{ loop.index }} + {%- if variant.has_fields() %} + return {{ enum_name|class_name_rb }}::{{ variant.name()|enum_name_rb }}.new( + {%- for field in variant.fields() %} + self.read{{ canonical_name(field.as_type().borrow()).borrow()|class_name_rb }}(){% if loop.last %}{% else %},{% endif %} + {%- endfor %} + ) + {%- else %} + return {{ enum_name|class_name_rb }}::{{ variant.name()|enum_name_rb }}.new + {% endif %} + end + {%- endfor %} + raise InternalError, 'Unexpected variant tag for {{ canonical_type_name }}' + {%- endif %} + end + + {% else %} + + {% let error_name = name %} + + # The Error type {{ error_name }} + + def read{{ canonical_type_name }} + variant = unpack_from 4, 'l>' + {% if e.is_flat() -%} + {%- for variant in e.variants() %} + if variant == {{ loop.index }} + return {{ error_name|class_name_rb }}::{{ variant.name()|class_name_rb }}.new( + readString() + ) + end + {%- endfor %} + + raise InternalError, 'Unexpected variant tag for {{ canonical_type_name }}' + {%- else -%} + {%- for variant in e.variants() %} + if variant == {{ loop.index }} + {%- if variant.has_fields() %} + return {{ error_name|class_name_rb }}::{{ variant.name()|class_name_rb }}.new( + {%- for field in variant.fields() %} + read{{ canonical_name(field.as_type().borrow()).borrow()|class_name_rb }}(){% if loop.last %}{% else %},{% endif %} + {%- endfor %} + ) + {%- else %} + return {{ error_name|class_name_rb }}::{{ variant.name()|class_name_rb }}.new + {%- endif %} + end + {%- endfor %} + + raise InternalError, 'Unexpected variant tag for {{ canonical_type_name }}' + {%- endif %} + end + {% endif %} + + {% when Type::Record { name: record_name, module_path } -%} + {%- let rec = ci|get_record_definition(record_name) -%} + # The Record type {{ record_name }}. + + def read{{ canonical_type_name }} + {{ rec.name()|class_name_rb }}.new( + {%- for field in rec.fields() %} + read{{ canonical_name(field.as_type().borrow()).borrow()|class_name_rb }}{% if loop.last %}{% else %},{% endif %} + {%- endfor %} + ) + end + + {% when Type::Optional { inner_type } -%} + # The Optional<T> type for {{ canonical_name(inner_type) }}. + + def read{{ canonical_type_name }} + flag = unpack_from 1, 'c' + + if flag == 0 + return nil + elsif flag == 1 + return read{{ canonical_name(inner_type).borrow()|class_name_rb }} + else + raise InternalError, 'Unexpected flag byte for {{ canonical_type_name }}' + end + end + + {% when Type::Sequence { inner_type } -%} + # The Sequence<T> type for {{ canonical_name(inner_type) }}. + + def read{{ canonical_type_name }} + count = unpack_from 4, 'l>' + + raise InternalError, 'Unexpected negative sequence length' if count.negative? + + items = [] + + count.times do + items.append read{{ canonical_name(inner_type).borrow()|class_name_rb }} + end + + items + end + + {% when Type::Map { key_type: k, value_type: inner_type } -%} + # The Map<T> type for {{ canonical_name(inner_type) }}. + + def read{{ canonical_type_name }} + count = unpack_from 4, 'l>' + raise InternalError, 'Unexpected negative map size' if count.negative? + + items = {} + count.times do + key = readString + items[key] = read{{ canonical_name(inner_type).borrow()|class_name_rb }} + end + + items + end + {%- else -%} + # This type is not yet supported in the Ruby backend. + def read{{ canonical_type_name }} + raise InternalError, 'RustBufferStream.read not implemented yet for {{ canonical_type_name }}' + end + + {%- endmatch -%} + {%- endfor %} + + def unpack_from(size, format) + raise InternalError, 'read past end of rust buffer' if @offset + size > @rbuf.len + + value = @rbuf.data.get_bytes(@offset, size).unpack format + + @offset += size + + # TODO: verify this + raise 'more than one element!!!' if value.size > 1 + + value[0] + end +end + +private_constant :RustBufferStream diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RustBufferTemplate.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RustBufferTemplate.rb new file mode 100644 index 0000000000..0194c9666d --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/RustBufferTemplate.rb @@ -0,0 +1,236 @@ +class RustBuffer < FFI::Struct + layout :capacity, :int32, + :len, :int32, + :data, :pointer + + def self.alloc(size) + return {{ ci.namespace()|class_name_rb }}.rust_call(:{{ ci.ffi_rustbuffer_alloc().name() }}, size) + end + + def self.reserve(rbuf, additional) + return {{ ci.namespace()|class_name_rb }}.rust_call(:{{ ci.ffi_rustbuffer_reserve().name() }}, rbuf, additional) + end + + def free + {{ ci.namespace()|class_name_rb }}.rust_call(:{{ ci.ffi_rustbuffer_free().name() }}, self) + end + + def capacity + self[:capacity] + end + + def len + self[:len] + end + + def len=(value) + self[:len] = value + end + + def data + self[:data] + end + + def to_s + "RustBuffer(capacity=#{capacity}, len=#{len}, data=#{data.read_bytes len})" + end + + # The allocated buffer will be automatically freed if an error occurs, ensuring that + # we don't accidentally leak it. + def self.allocWithBuilder + builder = RustBufferBuilder.new + + begin + yield builder + rescue => e + builder.discard + raise e + end + end + + # The RustBuffer will be freed once the context-manager exits, ensuring that we don't + # leak it even if an error occurs. + def consumeWithStream + stream = RustBufferStream.new self + + yield stream + + raise RuntimeError, 'junk data left in buffer after consuming' if stream.remaining != 0 + ensure + free + end + + {%- for typ in ci.iter_types() -%} + {%- let canonical_type_name = canonical_name(typ) -%} + {%- match typ -%} + + {% when Type::String -%} + # The primitive String type. + + def self.allocFromString(value) + RustBuffer.allocWithBuilder do |builder| + builder.write value.encode('utf-8') + return builder.finalize + end + end + + def consumeIntoString + consumeWithStream do |stream| + return stream.read(stream.remaining).force_encoding(Encoding::UTF_8) + end + end + + {% when Type::Bytes -%} + # The primitive Bytes type. + + def self.allocFromBytes(value) + RustBuffer.allocWithBuilder do |builder| + builder.write_Bytes(value) + return builder.finalize + end + end + + def consumeIntoBytes + consumeWithStream do |stream| + return stream.readBytes + end + end + + {% when Type::Timestamp -%} + def self.alloc_from_{{ canonical_type_name }}(v) + RustBuffer.allocWithBuilder do |builder| + builder.write_{{ canonical_type_name }}(v) + return builder.finalize + end + end + + def consumeInto{{ canonical_type_name }} + consumeWithStream do |stream| + return stream.read{{ canonical_type_name }} + end + end + + {% when Type::Duration -%} + def self.alloc_from_{{ canonical_type_name }}(v) + RustBuffer.allocWithBuilder do |builder| + builder.write_{{ canonical_type_name }}(v) + return builder.finalize + end + end + + def consumeInto{{ canonical_type_name }} + consumeWithStream do |stream| + return stream.read{{ canonical_type_name }} + end + end + + {% when Type::Record { name: record_name, module_path } -%} + {%- let rec = ci|get_record_definition(record_name) -%} + # The Record type {{ record_name }}. + + def self.alloc_from_{{ canonical_type_name }}(v) + RustBuffer.allocWithBuilder do |builder| + builder.write_{{ canonical_type_name }}(v) + return builder.finalize + end + end + + def consumeInto{{ canonical_type_name }} + consumeWithStream do |stream| + return stream.read{{ canonical_type_name }} + end + end + + {% when Type::Enum { name: enum_name, module_path } -%} + {% if !ci.is_name_used_as_error(enum_name) %} + {%- let e = ci|get_enum_definition(enum_name) -%} + # The Enum type {{ enum_name }}. + + def self.alloc_from_{{ canonical_type_name }}(v) + RustBuffer.allocWithBuilder do |builder| + builder.write_{{ canonical_type_name }}(v) + return builder.finalize + end + end + + def consumeInto{{ canonical_type_name }} + consumeWithStream do |stream| + return stream.read{{ canonical_type_name }} + end + end + {% endif %} + + {% when Type::Optional { inner_type } -%} + # The Optional<T> type for {{ canonical_name(inner_type) }}. + + def self.alloc_from_{{ canonical_type_name }}(v) + RustBuffer.allocWithBuilder do |builder| + builder.write_{{ canonical_type_name }}(v) + return builder.finalize() + end + end + + def consumeInto{{ canonical_type_name }} + consumeWithStream do |stream| + return stream.read{{ canonical_type_name }} + end + end + + {% when Type::Sequence { inner_type } -%} + # The Sequence<T> type for {{ canonical_name(inner_type) }}. + + def self.alloc_from_{{ canonical_type_name }}(v) + RustBuffer.allocWithBuilder do |builder| + builder.write_{{ canonical_type_name }}(v) + return builder.finalize() + end + end + + def consumeInto{{ canonical_type_name }} + consumeWithStream do |stream| + return stream.read{{ canonical_type_name }} + end + end + + {% when Type::Map { key_type: k, value_type: inner_type } -%} + # The Map<T> type for {{ canonical_name(inner_type) }}. + + def self.alloc_from_{{ canonical_type_name }}(v) + RustBuffer.allocWithBuilder do |builder| + builder.write_{{ canonical_type_name }}(v) + return builder.finalize + end + end + + def consumeInto{{ canonical_type_name }} + consumeWithStream do |stream| + return stream.read{{ canonical_type_name }} + end + end + + {%- else -%} + {#- No code emitted for types that don't lower into a RustBuffer -#} + {%- endmatch -%} + {%- endfor %} +end + +module UniFFILib + class ForeignBytes < FFI::Struct + layout :len, :int32, + :data, :pointer + + def len + self[:len] + end + + def data + self[:data] + end + + def to_s + "ForeignBytes(len=#{len}, data=#{data.read_bytes(len)})" + end + end +end + +private_constant :UniFFILib diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/TopLevelFunctionTemplate.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/TopLevelFunctionTemplate.rb new file mode 100644 index 0000000000..13214cf31b --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/TopLevelFunctionTemplate.rb @@ -0,0 +1,16 @@ +{%- match func.return_type() -%} +{%- when Some with (return_type) %} + +def self.{{ func.name()|fn_name_rb }}({%- call rb::arg_list_decl(func) -%}) + {%- call rb::coerce_args(func) %} + result = {% call rb::to_ffi_call(func) %} + return {{ "result"|lift_rb(return_type) }} +end + +{% when None %} + +def self.{{ func.name()|fn_name_rb }}({%- call rb::arg_list_decl(func) -%}) + {%- call rb::coerce_args(func) %} + {% call rb::to_ffi_call(func) %} +end +{% endmatch %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/macros.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/macros.rb new file mode 100644 index 0000000000..8dc3e5e613 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/macros.rb @@ -0,0 +1,73 @@ +{# +// Template to call into rust. Used in several places. +// Variable names in `arg_list_decl` should match up with arg lists +// passed to rust via `_arg_list_ffi_call` (we use `var_name_rb` in `lower_rb`) +#} + +{%- macro to_ffi_call(func) -%} + {%- match func.throws_name() -%} + {%- when Some with (e) -%} + {{ ci.namespace()|class_name_rb }}.rust_call_with_error({{ e|class_name_rb }}, + {%- else -%} + {{ ci.namespace()|class_name_rb }}.rust_call( + {%- endmatch -%} + :{{ func.ffi_func().name() }}, + {%- call _arg_list_ffi_call(func) -%} +) +{%- endmacro -%} + +{%- macro to_ffi_call_with_prefix(prefix, func) -%} + {%- match func.throws_name() -%} + {%- when Some with (e) -%} + {{ ci.namespace()|class_name_rb }}.rust_call_with_error({{ e|class_name_rb }}, + {%- else -%} + {{ ci.namespace()|class_name_rb }}.rust_call( + {%- endmatch -%} + :{{ func.ffi_func().name() }}, + {{- prefix }}, + {%- call _arg_list_ffi_call(func) -%} +) +{%- endmacro -%} + +{%- macro _arg_list_ffi_call(func) %} + {%- for arg in func.arguments() %} + {{- arg.name()|lower_rb(arg.as_type().borrow()) }} + {%- if !loop.last %},{% endif %} + {%- endfor %} +{%- endmacro -%} + +{#- +// Arglist as used in Ruby declarations of methods, functions and constructors. +// Note the var_name_rb and type_rb filters. +-#} + +{% macro arg_list_decl(func) %} + {%- for arg in func.arguments() -%} + {{ arg.name()|var_name_rb }} + {%- match arg.default_value() %} + {%- when Some with(literal) %} = {{ literal|literal_rb }} + {%- else %} + {%- endmatch %} + {%- if !loop.last %}, {% endif -%} + {%- endfor %} +{%- endmacro %} + +{#- +// Arglist as used in the UniFFILib function declarations. +// Note unfiltered name but type_ffi filters. +-#} +{%- macro arg_list_ffi_decl(func) %} + [{%- for arg in func.arguments() -%}{{ arg.type_().borrow()|type_ffi }}, {% endfor -%} RustCallStatus.by_ref] +{%- endmacro -%} + +{%- macro coerce_args(func) %} + {%- for arg in func.arguments() %} + {{ arg.name() }} = {{ arg.name()|coerce_rb(ci.namespace()|class_name_rb, arg.as_type().borrow()) -}} + {% endfor -%} +{%- endmacro -%} + +{%- macro coerce_args_extra_indent(func) %} + {%- for arg in func.arguments() %} + {{ arg.name() }} = {{ arg.name()|coerce_rb(ci.namespace()|class_name_rb, arg.as_type().borrow()) }} + {%- endfor %} +{%- endmacro -%} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/wrapper.rb b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/wrapper.rb new file mode 100644 index 0000000000..e3631b68de --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/templates/wrapper.rb @@ -0,0 +1,52 @@ +# This file was autogenerated by some hot garbage in the `uniffi` crate. +# Trust me, you don't want to mess with it! + +# Common helper code. +# +# Ideally this would live in a separate .rb file where it can be unittested etc +# in isolation, and perhaps even published as a re-useable package. +# +# However, it's important that the details of how this helper code works (e.g. the +# way that different builtin types are passed across the FFI) exactly match what's +# expected by the rust code on the other side of the interface. In practice right +# now that means coming from the exact some version of `uniffi` that was used to +# compile the rust component. The easiest way to ensure this is to bundle the Ruby +# helpers directly inline like we're doing here. + +require 'ffi' + + +module {{ ci.namespace()|class_name_rb }} + {% include "Helpers.rb" %} + + {% include "RustBufferTemplate.rb" %} + {% include "RustBufferStream.rb" %} + {% include "RustBufferBuilder.rb" %} + + # Error definitions + {% include "ErrorTemplate.rb" %} + + {% include "NamespaceLibraryTemplate.rb" %} + + # Public interface members begin here. + + {% for e in ci.enum_definitions() %} + {% if !ci.is_name_used_as_error(e.name()) %} + {% include "EnumTemplate.rb" %} + {% endif %} + {%- endfor -%} + + {%- for rec in ci.record_definitions() %} + {% include "RecordTemplate.rb" %} + {% endfor %} + + {% for func in ci.function_definitions() %} + {% include "TopLevelFunctionTemplate.rb" %} + {% endfor %} + + {% for obj in ci.object_definitions() %} + {% include "ObjectTemplate.rb" %} + {% endfor %} +end + +{% import "macros.rb" as rb %} diff --git a/third_party/rust/uniffi_bindgen/src/bindings/ruby/test.rs b/third_party/rust/uniffi_bindgen/src/bindings/ruby/test.rs new file mode 100644 index 0000000000..03da37d567 --- /dev/null +++ b/third_party/rust/uniffi_bindgen/src/bindings/ruby/test.rs @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::bindings::TargetLanguage; +use crate::library_mode::generate_bindings; +use anyhow::{bail, Context, Result}; +use camino::Utf8Path; +use std::env; +use std::ffi::OsString; +use std::process::{Command, Stdio}; +use uniffi_testing::UniFFITestHelper; + +/// Run Ruby tests for a UniFFI test fixture +pub fn run_test(tmp_dir: &str, fixture_name: &str, script_file: &str) -> Result<()> { + let status = test_script_command(tmp_dir, fixture_name, script_file)? + .spawn() + .context("Failed to spawn `ruby` when running script")? + .wait() + .context("Failed to wait for `ruby` when running script")?; + if !status.success() { + bail!("running `ruby` failed"); + } + Ok(()) +} + +/// Create a `Command` instance that runs a test script +pub fn test_script_command( + tmp_dir: &str, + fixture_name: &str, + script_file: &str, +) -> Result<Command> { + let script_path = Utf8Path::new(".").join(script_file).canonicalize_utf8()?; + let test_helper = UniFFITestHelper::new(fixture_name)?; + let out_dir = test_helper.create_out_dir(tmp_dir, &script_path)?; + let cdylib_path = test_helper.copy_cdylib_to_out_dir(&out_dir)?; + generate_bindings(&cdylib_path, None, &[TargetLanguage::Ruby], &out_dir, false)?; + + let rubypath = env::var_os("RUBYLIB").unwrap_or_else(|| OsString::from("")); + let rubypath = env::join_paths( + env::split_paths(&rubypath).chain(vec![out_dir.to_path_buf().into_std_path_buf()]), + )?; + + let mut command = Command::new("ruby"); + command + .current_dir(out_dir) + .env("RUBYLIB", rubypath) + .arg(script_path) + .stderr(Stdio::inherit()) + .stdout(Stdio::inherit()); + Ok(command) +} |