diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 000000000..9434d2a91 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,76 @@ +name: Rust + +on: + push: + branches: + - master + paths: + - ".github/workflows/rust.yml" + - "rust/**" + - "include/**" + - "src/**" + pull_request: + paths: + - ".github/workflows/rust.yml" + - "rust/**" + - "include/**" + - "src/**" + +env: + RUSTFLAGS: "-D warnings" + +jobs: + test: + name: cargo:test + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v4 + - name: Install Rust tools + run: | + rustup update --no-self-update stable + rustup default stable + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rust/target + key: ${{ runner.os }}-cargo-${{ hashFiles('rust/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + - name: Run tests + run: | + cd rust + cargo test --verbose + + lint: + name: cargo:lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Rust tools + run: | + rustup update --no-self-update stable + rustup default stable + rustup component add --toolchain stable clippy rustfmt + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rust/target + key: ${{ runner.os }}-cargo-${{ hashFiles('rust/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + - name: Check formatting + run: | + cd rust + cargo fmt --check + - name: Run clippy + run: | + cd rust + cargo clippy diff --git a/rust/.gitignore b/rust/.gitignore new file mode 100644 index 000000000..b83d22266 --- /dev/null +++ b/rust/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/rust/Cargo.lock b/rust/Cargo.lock new file mode 100644 index 000000000..906fc5859 --- /dev/null +++ b/rust/Cargo.lock @@ -0,0 +1,296 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "bindgen" +version = "0.72.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f72209734318d0b619a5e0f5129918b848c416e122a3c4ce054e03cb87b726f" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "cc" +version = "1.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" +dependencies = [ + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libloading" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "prettyplease" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "ruby-rbs-sys" +version = "0.1.0" +dependencies = [ + "bindgen", + "cc", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "windows-targets" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 000000000..36e83a904 --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,6 @@ +[workspace] +members = [ + "ruby-rbs-sys", +] + +resolver = "3" diff --git a/rust/ruby-rbs-sys/Cargo.toml b/rust/ruby-rbs-sys/Cargo.toml new file mode 100644 index 000000000..c70471abe --- /dev/null +++ b/rust/ruby-rbs-sys/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "ruby-rbs-sys" +version = "0.1.0" +edition = "2024" + +[lib] +doctest = false + +[build-dependencies] +bindgen = "0.72.0" +cc = "1.2.29" diff --git a/rust/ruby-rbs-sys/build.rs b/rust/ruby-rbs-sys/build.rs new file mode 100644 index 000000000..e731a0f62 --- /dev/null +++ b/rust/ruby-rbs-sys/build.rs @@ -0,0 +1,195 @@ +use std::{ + env, + error::Error, + fs, + path::{Path, PathBuf}, +}; + +fn main() -> Result<(), Box> { + let root = root_dir()?; + let include = root.join("include"); + let src = root.join("src"); + + build(&include, &src)?; + + let bindings = generate_bindings(&include)?; + write_bindings(&bindings)?; + + Ok(()) +} + +fn build(include_dir: &Path, src_dir: &Path) -> Result<(), Box> { + let mut build = cc::Build::new(); + + build.include(include_dir); + + // Suppress unused parameter warnings from C code + build.flag_if_supported("-Wno-unused-parameter"); + + build.files(source_files(src_dir)?); + build.try_compile("rbs")?; + + Ok(()) +} + +fn root_dir() -> Result> { + Ok(Path::new(env!("CARGO_MANIFEST_DIR")) + .ancestors() + .nth(2) + .ok_or("Failed to find project root directory")? + .to_path_buf()) +} + +fn source_files>(root_dir: P) -> Result, Box> { + let mut files = Vec::new(); + + for entry in fs::read_dir(root_dir.as_ref()).map_err(|e| { + format!( + "Failed to read source directory {:?}: {e}", + root_dir.as_ref() + ) + })? { + let entry = entry.map_err(|e| format!("Failed to read directory entry: {e}"))?; + let path = entry.path(); + + if path.is_file() { + let path_str = path + .to_str() + .ok_or_else(|| format!("Invalid UTF-8 in filename: {path:?}"))?; + + if Path::new(path_str) + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("c")) + { + files.push(path_str.to_string()); + } + } else if path.is_dir() { + files.extend(source_files(path)?); + } + } + + Ok(files) +} + +fn generate_bindings(include_path: &Path) -> Result> { + let bindings = bindgen::Builder::default() + .header("wrapper.h") + .clang_arg(format!("-I{}", include_path.display())) + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) + .generate_comments(true) + // Core types + .allowlist_type("rbs_ast_annotation_t") + .allowlist_type("rbs_ast_bool_t") + .allowlist_type("rbs_ast_comment_t") + .allowlist_type("rbs_ast_declarations_class_alias_t") + .allowlist_type("rbs_ast_declarations_class_super_t") + .allowlist_type("rbs_ast_declarations_class_t") + .allowlist_type("rbs_ast_declarations_constant_t") + .allowlist_type("rbs_ast_declarations_global_t") + .allowlist_type("rbs_ast_declarations_interface_t") + .allowlist_type("rbs_ast_declarations_module_alias_t") + .allowlist_type("rbs_ast_declarations_module_self_t") + .allowlist_type("rbs_ast_declarations_module_t") + .allowlist_type("rbs_ast_declarations_type_alias_t") + .allowlist_type("rbs_ast_directives_use_single_clause_t") + .allowlist_type("rbs_ast_directives_use_t") + .allowlist_type("rbs_ast_directives_use_wildcard_clause_t") + .allowlist_type("rbs_ast_integer_t") + .allowlist_type("rbs_ast_members_alias_t") + .allowlist_type("rbs_ast_members_attr_accessor_t") + .allowlist_type("rbs_ast_members_attr_reader_t") + .allowlist_type("rbs_ast_members_attr_writer_t") + .allowlist_type("rbs_ast_members_class_instance_variable_t") + .allowlist_type("rbs_ast_members_class_variable_t") + .allowlist_type("rbs_ast_members_extend_t") + .allowlist_type("rbs_ast_members_include_t") + .allowlist_type("rbs_ast_members_instance_variable_t") + .allowlist_type("rbs_ast_members_method_definition_overload_t") + .allowlist_type("rbs_ast_members_method_definition_t") + .allowlist_type("rbs_ast_members_prepend_t") + .allowlist_type("rbs_ast_members_private_t") + .allowlist_type("rbs_ast_members_public_t") + .allowlist_type("rbs_ast_ruby_annotations_class_alias_annotation_t") + .allowlist_type("rbs_ast_ruby_annotations_colon_method_type_annotation_t") + .allowlist_type("rbs_ast_ruby_annotations_instance_variable_annotation_t") + .allowlist_type("rbs_ast_ruby_annotations_method_types_annotation_t") + .allowlist_type("rbs_ast_ruby_annotations_module_alias_annotation_t") + .allowlist_type("rbs_ast_ruby_annotations_node_type_assertion_t") + .allowlist_type("rbs_ast_ruby_annotations_return_type_annotation_t") + .allowlist_type("rbs_ast_ruby_annotations_skip_annotation_t") + .allowlist_type("rbs_ast_ruby_annotations_type_application_annotation_t") + .allowlist_type("rbs_ast_string_t") + .allowlist_type("rbs_ast_symbol_t") + .allowlist_type("rbs_ast_type_param_t") + .allowlist_type("rbs_encoding_t") + .allowlist_type("rbs_encoding_type_t") + .allowlist_type("rbs_keyword_t") + .allowlist_type("rbs_method_type_t") + .allowlist_type("rbs_namespace_t") + .allowlist_type("rbs_node_list_t") + .allowlist_type("rbs_signature_t") + .allowlist_type("rbs_string_t") + .allowlist_type("rbs_type_name_t") + .allowlist_type("rbs_types_alias_t") + .allowlist_type("rbs_types_bases_any_t") + .allowlist_type("rbs_types_bases_bool_t") + .allowlist_type("rbs_types_bases_bottom_t") + .allowlist_type("rbs_types_bases_class_t") + .allowlist_type("rbs_types_bases_instance_t") + .allowlist_type("rbs_types_bases_nil_t") + .allowlist_type("rbs_types_bases_self_t") + .allowlist_type("rbs_types_bases_top_t") + .allowlist_type("rbs_types_bases_void_t") + .allowlist_type("rbs_types_block_t") + .allowlist_type("rbs_types_class_instance_t") + .allowlist_type("rbs_types_class_singleton_t") + .allowlist_type("rbs_types_function_param_t") + .allowlist_type("rbs_types_function_t") + .allowlist_type("rbs_types_interface_t") + .allowlist_type("rbs_types_intersection_t") + .allowlist_type("rbs_types_literal_t") + .allowlist_type("rbs_types_optional_t") + .allowlist_type("rbs_types_proc_t") + .allowlist_type("rbs_types_record_field_type_t") + .allowlist_type("rbs_types_record_t") + .allowlist_type("rbs_types_tuple_t") + .allowlist_type("rbs_types_union_t") + .allowlist_type("rbs_types_untyped_function_t") + .allowlist_type("rbs_types_variable_t") + .constified_enum_module("rbs_encoding_type_t") + .constified_enum_module("rbs_node_type") + // Encodings + .allowlist_var("rbs_encodings") + // Parser functions + .allowlist_function("rbs_constant_pool_id_to_constant") + .allowlist_function("rbs_parse_signature") + .allowlist_function("rbs_parser_free") + .allowlist_function("rbs_parser_new") + // String functions + .allowlist_function("rbs_string_new") + // Global constant pool + .allowlist_var("RBS_GLOBAL_CONSTANT_POOL") + .allowlist_function("rbs_constant_pool_free") + .allowlist_function("rbs_constant_pool_init") + .generate() + .map_err(|_| "Unable to generate rbs bindings")?; + + Ok(bindings) +} + +fn write_bindings(bindings: &bindgen::Bindings) -> Result<(), Box> { + let out_path = PathBuf::from( + env::var("OUT_DIR").map_err(|e| format!("OUT_DIR environment variable not set: {e}"))?, + ); + + bindings + .write_to_file(out_path.join("bindings.rs")) + .map_err(|e| { + format!( + "Failed to write bindings to {:?}: {e}", + out_path.join("bindings.rs") + ) + })?; + + Ok(()) +} diff --git a/rust/ruby-rbs-sys/src/lib.rs b/rust/ruby-rbs-sys/src/lib.rs new file mode 100644 index 000000000..aabb7e1d1 --- /dev/null +++ b/rust/ruby-rbs-sys/src/lib.rs @@ -0,0 +1,61 @@ +#![allow( + clippy::useless_transmute, + clippy::missing_safety_doc, + clippy::ptr_offset_with_cast, + dead_code, + non_camel_case_types, + non_upper_case_globals, + non_snake_case +)] +pub mod bindings { + include!(concat!(env!("OUT_DIR"), "/bindings.rs")); +} + +#[cfg(test)] +mod tests { + use super::bindings::*; + use std::sync::Once; + + use rbs_encoding_type_t::RBS_ENCODING_UTF_8; + + static INIT: Once = Once::new(); + + fn setup() { + INIT.call_once(|| unsafe { + rbs_constant_pool_init(RBS_GLOBAL_CONSTANT_POOL, 26); + }); + } + + #[test] + fn test_rbs_parser_bindings() { + setup(); + + let rbs_code = r#" + class User + attr_reader name: String + def initialize: (String) -> void + end + + module Authentication + def authenticate: (String, String) -> bool + end + "#; + + let bytes = rbs_code.as_bytes(); + let start_ptr = bytes.as_ptr() as *const i8; + let end_ptr = unsafe { start_ptr.add(bytes.len()) } as *const i8; + + let rbs_string = unsafe { rbs_string_new(start_ptr, end_ptr) }; + let encoding_ptr = + unsafe { &rbs_encodings[RBS_ENCODING_UTF_8 as usize] as *const rbs_encoding_t }; + let parser = unsafe { rbs_parser_new(rbs_string, encoding_ptr, 0, bytes.len() as i32) }; + + let mut signature: *mut rbs_signature_t = std::ptr::null_mut(); + let result = unsafe { rbs_parse_signature(parser, &mut signature) }; + + assert!(result, "Failed to parse RBS signature"); + assert!(!signature.is_null(), "Signature should not be null"); + + unsafe { rbs_parser_free(parser) }; + } +} diff --git a/rust/ruby-rbs-sys/wrapper.h b/rust/ruby-rbs-sys/wrapper.h new file mode 100644 index 000000000..74a1361b1 --- /dev/null +++ b/rust/ruby-rbs-sys/wrapper.h @@ -0,0 +1 @@ +#include "rbs.h"