diff --git a/.gitignore b/.gitignore index f9621c1c7..5fb797eca 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,7 @@ reach_wrap_output.json **/resolve_log.out* *.facts -*.facts.zst \ No newline at end of file +*.facts.zst + +# rust +*target* diff --git a/docs/development/building-from-source.md b/docs/development/building-from-source.md index ca7c5e86f..fe855bece 100644 --- a/docs/development/building-from-source.md +++ b/docs/development/building-from-source.md @@ -3,6 +3,7 @@ **RESOLVE** has been tested on **Ubuntu 24.04.4 LTS**, but should work on other distributions that can provide the following packages: - Nightly Rust +- cbindgen (`cargo install cbindgen --version 0.29.0 --locked`) - uv - CMake - build-essential diff --git a/resolve-facts/CMakeLists.txt b/resolve-facts/CMakeLists.txt index aa11efe9c..abfb69336 100644 --- a/resolve-facts/CMakeLists.txt +++ b/resolve-facts/CMakeLists.txt @@ -13,6 +13,44 @@ endif() include(GNUInstallDirs) include(CMakePackageConfigHelpers) +###################################################################### +# RUST FACTS ABI + +set(FACTS_RS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/rs") +set(FACTS_RS_TARGET_DIR "${FACTS_RS_DIR}/target/release") +set(FACTS_RS_STATICLIB "${FACTS_RS_TARGET_DIR}/libfacts_rs.a") +set(FACTS_RS_GENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated") +set(FACTS_RS_HEADER "${FACTS_RS_GENERATED_DIR}/facts_rs.hpp") +file(MAKE_DIRECTORY "${FACTS_RS_GENERATED_DIR}") + +find_program(CARGO_EXECUTABLE cargo REQUIRED) +find_program(CBINDGEN_EXECUTABLE cbindgen HINTS "$ENV{HOME}/.cargo/bin" REQUIRED) + +file(GLOB FACTS_RS_SOURCES CONFIGURE_DEPENDS "${FACTS_RS_DIR}/src/*.rs") +add_custom_command( + OUTPUT "${FACTS_RS_STATICLIB}" "${FACTS_RS_HEADER}" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${FACTS_RS_GENERATED_DIR}" + COMMAND "${CARGO_EXECUTABLE}" build --manifest-path "${FACTS_RS_DIR}/Cargo.toml" --release + COMMAND "${CBINDGEN_EXECUTABLE}" --config "${FACTS_RS_DIR}/cbindgen.toml" + --crate facts-rs --output "${FACTS_RS_HEADER}" "${FACTS_RS_DIR}" + DEPENDS ${FACTS_RS_SOURCES} "${FACTS_RS_DIR}/Cargo.toml" "${FACTS_RS_DIR}/Cargo.lock" + "${FACTS_RS_DIR}/cbindgen.toml" + WORKING_DIRECTORY "${FACTS_RS_DIR}" + COMMENT "Building facts-rs and generating its C++ ABI header" + VERBATIM +) +add_custom_target(facts_rs_build ALL DEPENDS "${FACTS_RS_STATICLIB}" "${FACTS_RS_HEADER}") + +add_library(facts_rs STATIC IMPORTED GLOBAL) +set_target_properties(facts_rs PROPERTIES + IMPORTED_LOCATION "${FACTS_RS_STATICLIB}" + INTERFACE_INCLUDE_DIRECTORIES "${FACTS_RS_GENERATED_DIR}" +) +add_dependencies(facts_rs facts_rs_build) + +install(FILES "${FACTS_RS_STATICLIB}" DESTINATION ${CMAKE_INSTALL_LIBDIR}) +install(FILES "${FACTS_RS_HEADER}" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) + add_subdirectory(vendor/argparse) add_subdirectory(vendor/json) diff --git a/resolve-facts/Config.cmake.in b/resolve-facts/Config.cmake.in index 6909bc4fb..9ca79c112 100644 --- a/resolve-facts/Config.cmake.in +++ b/resolve-facts/Config.cmake.in @@ -1,9 +1,16 @@ @PACKAGE_INIT@ -include("${CMAKE_CURRENT_LIST_DIR}/ResolveFactsTargets.cmake") - include(CMakeFindDependencyMacro) find_dependency(glaze) + +if(NOT TARGET facts_rs) + add_library(facts_rs STATIC IMPORTED) + set_target_properties(facts_rs PROPERTIES + IMPORTED_LOCATION "${PACKAGE_PREFIX_DIR}/@CMAKE_INSTALL_LIBDIR@/libfacts_rs.a" + INTERFACE_INCLUDE_DIRECTORIES "${PACKAGE_PREFIX_DIR}/@CMAKE_INSTALL_INCLUDEDIR@" + ) +endif() + include("${CMAKE_CURRENT_LIST_DIR}/ResolveFactsTargets.cmake") check_required_components(ResolveFacts) diff --git a/resolve-facts/README.md b/resolve-facts/README.md index bbd34d7e1..c3e72624f 100644 --- a/resolve-facts/README.md +++ b/resolve-facts/README.md @@ -11,3 +11,7 @@ Tools for creating and querying RESOLVE binary metadata, including the `reach` t - Facts: - `reach` tool: + +## Future Improvements + +Strings are interned at an LLVM Module-level. If we were to intern strings across every module together, we would be able to get more space savings, but likely at the expense of more CPU-heavy assembly/decompression. It's also unclear if/how we could compress ELF strings inline. diff --git a/resolve-facts/rs/Cargo.lock b/resolve-facts/rs/Cargo.lock new file mode 100644 index 000000000..f95a5febc --- /dev/null +++ b/resolve-facts/rs/Cargo.lock @@ -0,0 +1,65 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0e56a716f1e132ff6bf4bdac1c944a3fcdc1cae65f70a4a2a1ac3b401d2d1f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "facts-rs" +version = "0.1.0" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/resolve-facts/rs/Cargo.toml b/resolve-facts/rs/Cargo.toml new file mode 100644 index 000000000..725335a9f --- /dev/null +++ b/resolve-facts/rs/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "facts-rs" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["rlib", "staticlib", "cdylib"] + +[dependencies] +bytemuck = { version = "1", features = ["derive"] } diff --git a/resolve-facts/rs/cbindgen.toml b/resolve-facts/rs/cbindgen.toml new file mode 100644 index 000000000..babadc1ac --- /dev/null +++ b/resolve-facts/rs/cbindgen.toml @@ -0,0 +1,21 @@ +language = "C++" +namespace = "facts_rs" +pragma_once = true +include_guard = "RESOLVE_FACTS_RS_H" +header = """ +/* + * Generated by cbindgen from resolve-facts/rs. Do not edit by hand. + */ +""" + +[parse] +parse_deps = false + +[parse.expand] +crates = ["facts-rs"] + +[export] +include = [ + "Node", + "Edge", +] diff --git a/resolve-facts/rs/src/builder.rs b/resolve-facts/rs/src/builder.rs new file mode 100644 index 000000000..aff4d9621 --- /dev/null +++ b/resolve-facts/rs/src/builder.rs @@ -0,0 +1,377 @@ +use std::collections::*; +use std::mem::*; + +use crate::interner::*; +use crate::schema::*; +use crate::utils::*; +use crate::writer::*; + +pub type ModuleHandle = u32; +pub const INVALID_ID: u32 = u32::MAX; + +struct ModuleBuilder { + nodes: Vec, + edges: Vec, + pool: Interner, + edge_indexes: HashMap<(NodeID, NodeID), usize>, +} + +impl ModuleBuilder { + fn new(hint: usize) -> Self { + let edge_hint = hint.saturating_mul(2); + Self { + nodes: Vec::with_capacity(hint), + edges: Vec::with_capacity(edge_hint), + pool: Interner::default(), + edge_indexes: HashMap::with_capacity(edge_hint), + } + } + + fn add_node(&mut self, ty: NodeType) -> NodeID { + let dense_id = u32::try_from(self.nodes.len()).expect("module has too many nodes"); + self.nodes.push(Node::new(ty)); + dense_id + } + + fn node_mut(&mut self, id: NodeID) -> Option<&mut Node> { + self.nodes.get_mut(id as usize) + } + + fn add_edge(&mut self, src: NodeID, dst: NodeID, kind: EdgeKind) -> bool { + if src as usize >= self.nodes.len() || dst as usize >= self.nodes.len() { + return false; + } + + let id = (src, dst); + let kind = 1u32 << kind as u8; + if let Some(&index) = self.edge_indexes.get(&id) { + self.edges[index].kinds |= kind; + return true; + } + + self.edge_indexes.insert(id, self.edges.len()); + self.edges.push(Edge { + src, + dst, + kinds: kind, + }); + true + } + + fn intern_for_node(&mut self, id: NodeID, value: &str) -> Option<(NodeID, Interned)> { + self.nodes.get(id as usize)?; + let interned = self.pool.intern(value); + Some((id, interned)) + } + + fn word_len(&self) -> usize { + size_of::() / size_of::() + + self.nodes.len() * size_of::() / size_of::() + + self.edges.len() * size_of::() / size_of::() + + self.pool.bytes().len().div_ceil(size_of::()) + } + + fn serialize_into(mut self, words: &mut Vec) { + assert!( + self.nodes + .first() + .is_some_and(|node| node.node_type_raw() == NodeType::Module as u8), + "module node 0 must have type Module" + ); + let pool_len = self.pool.bytes().len(); + let padded_pool_len = pool_len.checked_add(3).expect("intern pool is too large") & !3; + let header = ModuleHeader { + version: FORMAT_VERSION, + node_count: u32::try_from(self.nodes.len()).expect("module has too many nodes"), + edge_count: u32::try_from(self.edges.len()).expect("module has too many edges"), + string_pool_len: u32::try_from(padded_pool_len) + .expect("module intern pool exceeds 4 GiB"), + }; + + words.extend_from_slice(bytemuck::cast_slice(std::slice::from_ref(&header))); + words.extend_from_slice(bytemuck::cast_slice(&self.nodes)); + + self.edges.sort_unstable_by_key(|edge| (edge.src, edge.dst)); + words.extend_from_slice(bytemuck::cast_slice(&self.edges)); + + for chunk in self.pool.bytes().chunks(size_of::()) { + let mut bytes = [0; size_of::()]; + bytes[..chunk.len()].copy_from_slice(chunk); + words.push(u32::from_ne_bytes(bytes)); + } + } +} + +#[derive(Default)] +pub struct FactsBuilder { + modules: Vec, +} + +impl FactsBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn add_module(&mut self, hint: usize) -> ModuleHandle { + let index = u32::try_from(self.modules.len()).expect("program has too many modules"); + self.modules.push(ModuleBuilder::new(hint)); + index + } + + pub fn add_node(&mut self, module: ModuleHandle, ty: NodeType) -> Option { + Some(self.module_mut(module)?.add_node(ty)) + } + + pub fn add_edge( + &mut self, + module: ModuleHandle, + src: NodeID, + dst: NodeID, + kind: EdgeKind, + ) -> bool { + self.module_mut(module) + .is_some_and(|module| module.add_edge(src, dst, kind)) + } + + pub fn set_node_idx(&mut self, module: ModuleHandle, node: NodeID, value: u32) -> bool { + self.set_node(module, node, |node| { + node.idx = value; + node.set_present(P_IDX); + }) + } + + pub fn set_node_name(&mut self, module: ModuleHandle, node: NodeID, value: &str) -> bool { + self.set_node_string(module, node, value, |node, value| { + node.name = value; + node.set_present(P_NAME); + }) + } + + pub fn set_node_opcode(&mut self, module: ModuleHandle, node: NodeID, value: &str) -> bool { + self.set_node_string(module, node, value, |node, value| { + node.opcode = value; + node.set_present(P_OPCODE); + }) + } + + pub fn set_node_linkage(&mut self, module: ModuleHandle, node: NodeID, value: Linkage) -> bool { + self.set_node(module, node, |node| node.set_linkage(value)) + } + + pub fn set_node_call_type( + &mut self, + module: ModuleHandle, + node: NodeID, + value: CallType, + ) -> bool { + self.set_node(module, node, |node| node.set_call_type(value)) + } + + pub fn set_node_source_loc( + &mut self, + module: ModuleHandle, + node: NodeID, + line: u32, + col: u32, + ) -> bool { + self.set_node(module, node, |node| { + node.source_line = line; + node.source_col = col; + node.set_present(P_SOURCE_LOC); + }) + } + + pub fn set_node_source_file( + &mut self, + module: ModuleHandle, + node: NodeID, + value: &str, + ) -> bool { + self.set_node_string(module, node, value, |node, value| { + node.source_file = value; + node.set_present(P_SOURCE_FILE); + }) + } + + pub fn set_node_function_type( + &mut self, + module: ModuleHandle, + node: NodeID, + value: &str, + ) -> bool { + self.set_node_string(module, node, value, |node, value| { + node.function_type = value; + node.set_present(P_FUNCTION_TYPE); + }) + } + + pub fn set_node_address_taken( + &mut self, + module: ModuleHandle, + node: NodeID, + value: bool, + ) -> bool { + self.set_node(module, node, |node| { + if value { + node.set_present(P_ADDRESS_TAKEN); + } else { + node.meta &= !P_ADDRESS_TAKEN; + } + }) + } + + pub fn freeze(self) -> FactsBuf { + let capacity = self.modules.iter().map(ModuleBuilder::word_len).sum(); + let mut words = Vec::with_capacity(capacity); + for module in self.modules { + module.serialize_into(&mut words); + } + + FactsBuf::from_words(words) + } + + fn module_mut(&mut self, module: ModuleHandle) -> Option<&mut ModuleBuilder> { + self.modules.get_mut(module as usize) + } + + fn set_node( + &mut self, + module: ModuleHandle, + node: NodeID, + update: impl FnOnce(&mut Node), + ) -> bool { + let Some(node) = self + .module_mut(module) + .and_then(|module| module.node_mut(node)) + else { + return false; + }; + update(node); + true + } + + fn set_node_string( + &mut self, + module: ModuleHandle, + node: NodeID, + value: &str, + update: impl FnOnce(&mut Node, Interned), + ) -> bool { + let Some(module) = self.module_mut(module) else { + return false; + }; + let Some((node, interned)) = module.intern_for_node(node, value) else { + return false; + }; + update(&mut module.nodes[node as usize], interned); + true + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn facts_builder_new() -> *mut FactsBuilder { + Box::into_raw(Box::new(FactsBuilder::new())) +} + +#[unsafe(no_mangle)] +pub extern "C" fn facts_builder_free(builder: *mut FactsBuilder) { + if !builder.is_null() { + unsafe { + drop(Box::from_raw(builder)); + } + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn facts_builder_add_module( + builder: *mut FactsBuilder, + hint: usize, +) -> ModuleHandle { + unsafe { builder.as_mut() }.map_or(INVALID_ID, |builder| builder.add_module(hint)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn facts_builder_add_node( + builder: *mut FactsBuilder, + module: ModuleHandle, + ty: NodeType, +) -> NodeID { + unsafe { builder.as_mut() } + .and_then(|builder| builder.add_node(module, ty)) + .unwrap_or(INVALID_ID) +} + +#[unsafe(no_mangle)] +pub extern "C" fn facts_builder_add_edge( + builder: *mut FactsBuilder, + module: ModuleHandle, + src: NodeID, + dst: NodeID, + kind: EdgeKind, +) -> bool { + unsafe { builder.as_mut() }.is_some_and(|builder| builder.add_edge(module, src, dst, kind)) +} + +macro_rules! ffi_node_setter { + ($ffi:ident, $method:ident, $ty:ty) => { + #[unsafe(no_mangle)] + pub extern "C" fn $ffi( + builder: *mut FactsBuilder, + module: ModuleHandle, + node: NodeID, + value: $ty, + ) -> bool { + unsafe { builder.as_mut() }.is_some_and(|builder| builder.$method(module, node, value)) + } + }; +} + +macro_rules! ffi_node_string_setter { + ($ffi:ident, $method:ident) => { + #[unsafe(no_mangle)] + pub extern "C" fn $ffi( + builder: *mut FactsBuilder, + module: ModuleHandle, + node: NodeID, + ptr: *const u8, + len: usize, + ) -> bool { + let Some(builder) = (unsafe { builder.as_mut() }) else { + return false; + }; + let Some(value) = (unsafe { as_str(ptr, len) }) else { + return false; + }; + builder.$method(module, node, value) + } + }; +} + +ffi_node_setter!(facts_builder_set_node_idx, set_node_idx, u32); +ffi_node_string_setter!(facts_builder_set_node_name, set_node_name); +ffi_node_string_setter!(facts_builder_set_node_opcode, set_node_opcode); +ffi_node_setter!(facts_builder_set_node_linkage, set_node_linkage, Linkage); +ffi_node_setter!( + facts_builder_set_node_call_type, + set_node_call_type, + CallType +); +ffi_node_string_setter!(facts_builder_set_node_source_file, set_node_source_file); +ffi_node_string_setter!(facts_builder_set_node_function_type, set_node_function_type); +ffi_node_setter!( + facts_builder_set_node_address_taken, + set_node_address_taken, + bool +); + +#[unsafe(no_mangle)] +pub extern "C" fn facts_builder_set_node_source_loc( + builder: *mut FactsBuilder, + module: ModuleHandle, + node: NodeID, + line: u32, + col: u32, +) -> bool { + unsafe { builder.as_mut() } + .is_some_and(|builder| builder.set_node_source_loc(module, node, line, col)) +} diff --git a/resolve-facts/rs/src/interner.rs b/resolve-facts/rs/src/interner.rs new file mode 100644 index 000000000..44ca9b2d0 --- /dev/null +++ b/resolve-facts/rs/src/interner.rs @@ -0,0 +1,32 @@ +use std::collections::*; + +use crate::schema::*; + +#[derive(Default)] +pub struct Interner { + ids: HashMap, + bytes: Vec, +} + +impl Interner { + pub fn intern(&mut self, value: &str) -> Interned { + if let Some(&id) = self.ids.get(value) { + return id; + } + + let offset = u32::try_from(self.bytes.len()).expect("intern pool exceeds 4 GiB"); + let bytes = value.as_bytes(); + let len = u32::try_from(bytes.len()).expect("interned string exceeds 4 GiB"); + + self.bytes.extend_from_slice(&len.to_le_bytes()); + self.bytes.extend_from_slice(bytes); + + let id = Interned(offset); + self.ids.insert(value.to_owned(), id); + id + } + + pub fn bytes(&self) -> &[u8] { + &self.bytes + } +} diff --git a/resolve-facts/rs/src/lib.rs b/resolve-facts/rs/src/lib.rs new file mode 100644 index 000000000..66c734799 --- /dev/null +++ b/resolve-facts/rs/src/lib.rs @@ -0,0 +1,12 @@ +#[cfg(not(target_endian = "little"))] +compile_error!("the facts format currently requires a little-endian target"); + +mod builder; +mod interner; +mod schema; +mod utils; +mod writer; + +pub use builder::{FactsBuilder, ModuleHandle}; +pub use schema::*; +pub use writer::FactsBuf; diff --git a/resolve-facts/rs/src/schema.rs b/resolve-facts/rs/src/schema.rs new file mode 100644 index 000000000..2b565fbc2 --- /dev/null +++ b/resolve-facts/rs/src/schema.rs @@ -0,0 +1,541 @@ +// notes: +// - some fields are over-sized to pad the struct to alignment + +use std::mem::*; + +use bytemuck::*; + +pub const FORMAT_VERSION: u32 = 1; + +macro_rules! enum_from_u8 { + ($ty:ty, $($value:path),+ $(,)?) => { + impl TryFrom for $ty { + type Error = u8; + + fn try_from(value: u8) -> Result { + match value { + $(value if value == $value as u8 => Ok($value),)+ + value => Err(value), + } + } + } + }; +} + +// Node identifier: index in its parent modules Node array +pub type NodeID = u32; + +// Offset into modules interned string pool +#[repr(transparent)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Pod, Zeroable)] +pub struct Interned(pub u32); + +#[allow(dead_code)] // cbindgen +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NodeType { + Module = 0, + Function = 1, + Argument = 2, + BasicBlock = 3, + Instruction = 4, + GlobalVariable = 5, +} + +#[allow(dead_code)] // cbindgen +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Linkage { + Other = 0, + ExternalLinkage = 1, +} + +#[allow(dead_code)] // cbindgen +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CallType { + Direct = 0, + Indirect = 1, +} + +enum_from_u8!( + NodeType, + NodeType::Module, + NodeType::Function, + NodeType::Argument, + NodeType::BasicBlock, + NodeType::Instruction, + NodeType::GlobalVariable, +); +enum_from_u8!(Linkage, Linkage::Other, Linkage::ExternalLinkage); +enum_from_u8!(CallType, CallType::Direct, CallType::Indirect); + +// positions in Node.meta indicating which properties are present. +pub const P_IDX: u32 = 1 << 0; +pub const P_NAME: u32 = 1 << 1; +pub const P_OPCODE: u32 = 1 << 2; +pub const P_LINKAGE: u32 = 1 << 3; +pub const P_CALL_TYPE: u32 = 1 << 4; +pub const P_SOURCE_LOC: u32 = 1 << 5; +pub const P_SOURCE_FILE: u32 = 1 << 6; +pub const P_FUNCTION_TYPE: u32 = 1 << 7; +pub const P_ADDRESS_TAKEN: u32 = 1 << 8; + +#[allow(dead_code)] // cbindgen +pub const PRESENT_MASK: u32 = u16::MAX as u32; +pub const NODE_TYPE_SHIFT: u32 = 16; +#[allow(dead_code)] // cbindgen +pub const NODE_TYPE_MASK: u32 = 0xff << NODE_TYPE_SHIFT; +pub const LINKAGE_SHIFT: u32 = 24; +pub const LINKAGE_MASK: u32 = 0x0f << LINKAGE_SHIFT; +pub const CALL_TYPE_SHIFT: u32 = 28; +pub const CALL_TYPE_MASK: u32 = 0x0f << CALL_TYPE_SHIFT; + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Pod, Zeroable)] +pub struct Node { + pub meta: u32, + pub idx: u32, + pub name: Interned, + pub opcode: Interned, + pub source_line: u32, + pub source_col: u32, + pub source_file: Interned, + pub function_type: Interned, +} + +#[allow(dead_code)] // reader +impl Node { + pub const fn new(ty: NodeType) -> Self { + Self { + meta: (ty as u32) << NODE_TYPE_SHIFT, + idx: 0, + name: Interned(0), + opcode: Interned(0), + source_line: 0, + source_col: 0, + source_file: Interned(0), + function_type: Interned(0), + } + } + + pub const fn present(&self) -> u16 { + (self.meta & PRESENT_MASK) as u16 + } + + pub const fn has(&self, property: u32) -> bool { + self.meta & property != 0 + } + + pub const fn node_type_raw(&self) -> u8 { + ((self.meta & NODE_TYPE_MASK) >> NODE_TYPE_SHIFT) as u8 + } + + pub const fn linkage_raw(&self) -> Option { + if self.has(P_LINKAGE) { + Some(((self.meta & LINKAGE_MASK) >> LINKAGE_SHIFT) as u8) + } else { + None + } + } + + pub const fn call_type_raw(&self) -> Option { + if self.has(P_CALL_TYPE) { + Some(((self.meta & CALL_TYPE_MASK) >> CALL_TYPE_SHIFT) as u8) + } else { + None + } + } + + pub fn node_type(&self) -> Result { + self.node_type_raw().try_into() + } + + pub fn linkage(&self) -> Result, u8> { + self.linkage_raw().map(TryInto::try_into).transpose() + } + + pub fn call_type(&self) -> Result, u8> { + self.call_type_raw().map(TryInto::try_into).transpose() + } + + pub const fn idx(&self) -> Option { + if self.has(P_IDX) { + Some(self.idx) + } else { + None + } + } + + pub const fn source_loc(&self) -> Option<(u32, u32)> { + if self.has(P_SOURCE_LOC) { + Some((self.source_line, self.source_col)) + } else { + None + } + } + + pub const fn address_taken(&self) -> bool { + self.has(P_ADDRESS_TAKEN) + } + + pub fn set_present(&mut self, property: u32) { + self.meta |= property; + } + + pub fn set_linkage(&mut self, linkage: Linkage) { + self.meta = (self.meta & !LINKAGE_MASK) | ((linkage as u32) << LINKAGE_SHIFT) | P_LINKAGE; + } + + pub fn set_call_type(&mut self, call_type: CallType) { + self.meta = + (self.meta & !CALL_TYPE_MASK) | ((call_type as u32) << CALL_TYPE_SHIFT) | P_CALL_TYPE; + } +} + +#[allow(dead_code)] // cbindgen +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum EdgeKind { + Calls = 0, + Contains = 1, + DataFlowTo = 2, + References = 3, + EntryPoint = 4, + ControlFlowTo = 5, +} + +enum_from_u8!( + EdgeKind, + EdgeKind::Calls, + EdgeKind::Contains, + EdgeKind::DataFlowTo, + EdgeKind::References, + EdgeKind::EntryPoint, + EdgeKind::ControlFlowTo, +); + +// A unique (src, dst) pair within a module. Edge arrays are sorted. +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Pod, Zeroable)] +pub struct Edge { + pub src: NodeID, + pub dst: NodeID, + pub kinds: u32, +} + +#[allow(dead_code)] // reader +impl Edge { + pub const fn has_kind(&self, kind: EdgeKind) -> bool { + self.kinds & (1 << kind as u8) != 0 + } + + pub fn kinds(&self) -> impl Iterator + '_ { + [ + EdgeKind::Calls, + EdgeKind::Contains, + EdgeKind::DataFlowTo, + EdgeKind::References, + EdgeKind::EntryPoint, + EdgeKind::ControlFlowTo, + ] + .into_iter() + .filter(|kind| self.has_kind(*kind)) + } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Pod, Zeroable)] +pub struct ModuleHeader { + pub version: u32, + pub node_count: u32, + pub edge_count: u32, + pub string_pool_len: u32, +} + +#[allow(dead_code)] // reader +impl ModuleHeader { + pub fn byte_len(&self) -> Option { + let nodes = usize::try_from(self.node_count) + .ok()? + .checked_mul(size_of::())?; + let edges = usize::try_from(self.edge_count) + .ok()? + .checked_mul(size_of::())?; + let strings = usize::try_from(self.string_pool_len).ok()?; + + size_of::() + .checked_add(nodes)? + .checked_add(edges)? + .checked_add(strings) + } +} + +// "View" API: + +#[derive(Clone, Copy, Debug)] +#[allow(dead_code)] // reader +pub struct ModuleRef<'a> { + bytes: &'a [u8], +} + +#[derive(Clone, Copy, Debug)] +pub struct NodeRef<'a> { + module: ModuleRef<'a>, + id: NodeID, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[allow(dead_code)] // reader +pub enum ViewError { + Misaligned, + Truncated, + UnsupportedVersion, + UnalignedModuleLength, +} + +impl std::fmt::Display for ViewError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Misaligned => f.write_str("facts bytes are not 4-byte aligned"), + Self::Truncated => f.write_str("facts module is truncated or has an invalid length"), + Self::UnsupportedVersion => f.write_str("facts module has an unsupported version"), + Self::UnalignedModuleLength => { + f.write_str("facts module length is not a multiple of 4 bytes") + } + } + } +} + +impl std::error::Error for ViewError {} + +#[allow(dead_code)] // reader +impl<'a> ModuleRef<'a> { + // Borrows the first complete module in "bytes" and returns the unconsumed suffix + pub fn from_prefix(bytes: &'a [u8]) -> Result<(Self, &'a [u8]), ViewError> { + if bytes.len() < size_of::() { + return Err(ViewError::Truncated); + } + if bytes.as_ptr().align_offset(align_of::()) != 0 { + return Err(ViewError::Misaligned); + } + + let header = bytemuck::from_bytes::(&bytes[..size_of::()]); + if header.version != FORMAT_VERSION { + return Err(ViewError::UnsupportedVersion); + } + + let module_len = header.byte_len().ok_or(ViewError::Truncated)?; + if module_len % align_of::() != 0 { + return Err(ViewError::UnalignedModuleLength); + } + if bytes.len() < module_len { + return Err(ViewError::Truncated); + } + + let (module, rest) = bytes.split_at(module_len); + Ok((Self { bytes: module }, rest)) + } + + pub fn header(&self) -> &'a ModuleHeader { + bytemuck::from_bytes(&self.bytes[..size_of::()]) + } + + pub const fn as_bytes(&self) -> &'a [u8] { + self.bytes + } + + pub fn nodes(&self) -> &'a [Node] { + let header = self.header(); + let start = size_of::(); + let end = start + header.node_count as usize * size_of::(); + bytemuck::cast_slice(&self.bytes[start..end]) + } + + pub fn edges(&self) -> &'a [Edge] { + let header = self.header(); + let nodes_len = header.node_count as usize * size_of::(); + let start = size_of::() + nodes_len; + let end = start + header.edge_count as usize * size_of::(); + bytemuck::cast_slice(&self.bytes[start..end]) + } + + pub fn string_pool(&self) -> &'a [u8] { + let header = self.header(); + let nodes_len = header.node_count as usize * size_of::(); + let edges_len = header.edge_count as usize * size_of::(); + let start = size_of::() + nodes_len + edges_len; + &self.bytes[start..start + header.string_pool_len as usize] + } + + // Resolves an interned ID without allocating or copying the string bytes. + pub fn string_bytes(&self, id: Interned) -> Option<&'a [u8]> { + let pool = self.string_pool(); + let offset = usize::try_from(id.0).ok()?; + let length_end = offset.checked_add(size_of::())?; + let length_bytes: [u8; 4] = pool.get(offset..length_end)?.try_into().ok()?; + let length = u32::from_le_bytes(length_bytes) as usize; + let value_end = length_end.checked_add(length)?; + pool.get(length_end..value_end) + } + + pub fn string(&self, id: Interned) -> Option<&'a str> { + std::str::from_utf8(self.string_bytes(id)?).ok() + } + + pub fn node(&self, id: NodeID) -> Option<&'a Node> { + self.nodes().get(id as usize) + } + + pub fn node_ref(&self, id: NodeID) -> Option> { + self.node(id)?; + Some(NodeRef { module: *self, id }) + } + + pub fn node_refs(&self) -> NodeIter<'a> { + NodeIter { + module: *self, + next: 0, + } + } +} + +impl<'a> NodeRef<'a> { + pub const fn id(self) -> NodeID { + self.id + } + + pub fn raw(self) -> &'a Node { + &self.module.nodes()[self.id as usize] + } + + pub fn node_type(self) -> Result { + self.raw().node_type() + } + + pub fn idx(self) -> Option { + self.raw().idx() + } + + pub fn name(self) -> Option<&'a str> { + self.string(P_NAME, self.raw().name) + } + + pub fn opcode(self) -> Option<&'a str> { + self.string(P_OPCODE, self.raw().opcode) + } + + pub fn linkage(self) -> Result, u8> { + self.raw().linkage() + } + + pub fn call_type(self) -> Result, u8> { + self.raw().call_type() + } + + pub fn source_loc(self) -> Option<(u32, u32)> { + self.raw().source_loc() + } + + pub fn source_file(self) -> Option<&'a str> { + self.string(P_SOURCE_FILE, self.raw().source_file) + } + + pub fn function_type(self) -> Option<&'a str> { + self.string(P_FUNCTION_TYPE, self.raw().function_type) + } + + pub fn address_taken(self) -> bool { + self.raw().address_taken() + } + + fn string(self, property: u32, id: Interned) -> Option<&'a str> { + self.raw().has(property).then(|| self.module.string(id))? + } +} + +#[derive(Clone, Debug)] +pub struct NodeIter<'a> { + module: ModuleRef<'a>, + next: NodeID, +} + +impl<'a> Iterator for NodeIter<'a> { + type Item = NodeRef<'a>; + + fn next(&mut self) -> Option { + let node = self.module.node_ref(self.next)?; + self.next += 1; + Some(node) + } + + fn size_hint(&self) -> (usize, Option) { + let remaining = self.module.nodes().len() - self.next as usize; + (remaining, Some(remaining)) + } +} + +impl ExactSizeIterator for NodeIter<'_> {} + +// A non-owning view over a complete concatenation of modules +#[derive(Clone, Copy, Debug)] +#[allow(dead_code)] // reader +pub struct FactsRef<'a> { + bytes: &'a [u8], +} + +#[allow(dead_code)] // reader +impl<'a> FactsRef<'a> { + pub const fn new(bytes: &'a [u8]) -> Self { + Self { bytes } + } + + pub const fn modules(self) -> ModuleIter<'a> { + ModuleIter { + remaining: self.bytes, + } + } + + pub const fn as_bytes(self) -> &'a [u8] { + self.bytes + } +} + +#[allow(dead_code)] // reader +pub struct ModuleIter<'a> { + remaining: &'a [u8], +} + +impl<'a> Iterator for ModuleIter<'a> { + type Item = Result, ViewError>; + + fn next(&mut self) -> Option { + if self.remaining.is_empty() { + return None; + } + + match ModuleRef::from_prefix(self.remaining) { + Ok((module, rest)) => { + self.remaining = rest; + Some(Ok(module)) + } + Err(error) => { + self.remaining = &[]; + Some(Err(error)) + } + } + } +} + +/// cbindgen:ignore +#[allow(dead_code)] +const LAYOUT_ASSERTIONS: () = { + assert!(size_of::() == 4); + assert!(align_of::() == 4); + assert!(size_of::() == 16); + assert!(align_of::() == 4); + assert!(size_of::() == 32); + assert!(align_of::() == 4); + assert!(size_of::() == 12); + assert!(align_of::() == 4); +}; diff --git a/resolve-facts/rs/src/utils.rs b/resolve-facts/rs/src/utils.rs new file mode 100644 index 000000000..9aba5a8b3 --- /dev/null +++ b/resolve-facts/rs/src/utils.rs @@ -0,0 +1,6 @@ +pub unsafe fn as_str<'a>(ptr: *const u8, len: usize) -> Option<&'a str> { + if ptr.is_null() { + return None; + } + std::str::from_utf8(unsafe { std::slice::from_raw_parts(ptr, len) }).ok() +} diff --git a/resolve-facts/rs/src/writer.rs b/resolve-facts/rs/src/writer.rs new file mode 100644 index 000000000..dff8ba7ae --- /dev/null +++ b/resolve-facts/rs/src/writer.rs @@ -0,0 +1,56 @@ +use crate::builder::*; +use crate::schema::*; + +#[derive(Debug, Default)] +pub struct FactsBuf(Vec); + +impl FactsBuf { + pub(crate) fn from_words(words: Vec) -> Self { + Self(words) + } + + pub fn as_bytes(&self) -> &[u8] { + bytemuck::cast_slice(&self.0) + } + + pub fn view(&self) -> FactsRef<'_> { + FactsRef::new(self.as_bytes()) + } + + pub fn len(&self) -> usize { + self.as_bytes().len() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn facts_buf_len(b: *const FactsBuf) -> usize { + unsafe { b.as_ref() }.map_or(0, |buf| buf.as_bytes().len()) +} + +#[unsafe(no_mangle)] +pub extern "C" fn facts_buf_data(b: *const FactsBuf) -> *const u8 { + unsafe { b.as_ref() }.map_or(std::ptr::null(), |buf| buf.as_bytes().as_ptr()) +} + +#[unsafe(no_mangle)] +pub extern "C" fn facts_buf_free(b: *mut FactsBuf) { + if !b.is_null() { + unsafe { + drop(Box::from_raw(b)); + } + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn facts_builder_freeze(builder: *mut FactsBuilder) -> *mut FactsBuf { + if builder.is_null() { + return std::ptr::null_mut(); + } + + let builder = unsafe { Box::from_raw(builder) }; + Box::into_raw(Box::new(builder.freeze())) +} diff --git a/scripts/install-deps-ci.sh b/scripts/install-deps-ci.sh index a55355365..427a9ac1f 100755 --- a/scripts/install-deps-ci.sh +++ b/scripts/install-deps-ci.sh @@ -62,4 +62,6 @@ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ | sh -s -- -y --default-toolchain nightly source "$HOME/.cargo/env" +echo "[*] Installing cbindgen" +cargo install cbindgen --version 0.29.0 --locked echo " All dependencies installed successfully."