From b5bbc3312aff853c4ee4197b02925e8539e19bbf Mon Sep 17 00:00:00 2001 From: Alexander Momchilov Date: Tue, 9 Jun 2026 15:15:18 -0400 Subject: [PATCH 1/2] Allocate Graph memory from Ruby GC Previously, the Graph objects were initialized in a new Rust-allocated Box. Let's switch to allocating this memory from the Ruby GC. This has several benefits: 1. Like `xmalloc()`, it gives the GC a chance to run if there isn't much free memory available 2. It puts us on the path to using `RUBY_TYPED_EMBEDDABLE` for more optimal allocations --- ext/rubydex/graph.c | 17 +++++++++--- ext/rubydex/handle.h | 5 ++-- rust/rubydex-sys/src/graph_api.rs | 45 +++++++++++++++++++++---------- 3 files changed, 47 insertions(+), 20 deletions(-) diff --git a/ext/rubydex/graph.c b/ext/rubydex/graph.c index 5434990bd..0a88c0496 100644 --- a/ext/rubydex/graph.c +++ b/ext/rubydex/graph.c @@ -38,10 +38,14 @@ static const char *extract_self_receiver(VALUE opts) { return StringValueCStr(kwarg_val); } -// Free function for the custom Graph allocator. We always have to call into Rust to free data allocated by it +// Free function for the custom Graph allocator. static void graph_free(void *ptr) { if (ptr) { - rdx_graph_free(ptr); + // let the Rust side drop the Graph struct internally. + rdx_graph_drop(ptr); + + // Free the TypeData Ruby object itself + xfree(ptr); } } @@ -61,8 +65,13 @@ const rb_data_type_t graph_type = { // Custom allocator for the Graph class. Calls into Rust to create a new `Arc>` that gets stored internally // as a void pointer static VALUE rdxr_graph_alloc(VALUE klass) { - void *graph = rdx_graph_new(); - return TypedData_Wrap_Struct(klass, &graph_type, graph); + void *graph; + // Can't use `TypedData_Make_Struct`, because the Graph is a Rust type that isn't exposed directly to C. + VALUE graph_obj = rb_data_typed_object_make(klass, &graph_type, &graph, RDX_GRAPH_SIZE); + + rdx_graph_init(graph); + + return graph_obj; } // Graph#index_all: (Array[String] file_paths) -> Array[String] diff --git a/ext/rubydex/handle.h b/ext/rubydex/handle.h index dba57acd5..427608223 100644 --- a/ext/rubydex/handle.h +++ b/ext/rubydex/handle.h @@ -35,14 +35,15 @@ static const rb_data_type_t handle_type = { }; static VALUE rdxr_handle_alloc(VALUE klass) { - HandleData *data = ALLOC(HandleData); + HandleData *data; + VALUE handle_obj = TypedData_Make_Struct(klass, HandleData, &handle_type, data); *data = (HandleData) { .graph_obj = Qnil, .id = 0, }; - return TypedData_Wrap_Struct(klass, &handle_type, data); + return handle_obj; } static VALUE rdxr_handle_initialize(VALUE self, VALUE graph_obj, VALUE id_val) { diff --git a/rust/rubydex-sys/src/graph_api.rs b/rust/rubydex-sys/src/graph_api.rs index 13a0cd68e..b08fdb367 100644 --- a/rust/rubydex-sys/src/graph_api.rs +++ b/rust/rubydex-sys/src/graph_api.rs @@ -23,17 +23,38 @@ use std::{mem, ptr}; pub type GraphPointer = *mut c_void; -/// Creates a new graph within a mutex. This is meant to be used when creating new Graph objects in Ruby +/// Returns the number of bytes needed to store a Graph in externally allocated memory. #[unsafe(no_mangle)] -pub extern "C" fn rdx_graph_new() -> GraphPointer { - Box::into_raw(Box::new(Graph::new())) as GraphPointer +pub static RDX_GRAPH_SIZE: usize = mem::size_of::(); + +/// Initializes a Graph in-place at the given `pointer`. +/// +/// # Safety +/// +/// `pointer` must point to valid, properly aligned memory of at least `RDX_GRAPH_SIZE` bytes. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rdx_graph_init(pointer: GraphPointer) { + // Ruby's allocator only guarantees 16 byte alignment on the 64-bit platforms we support. + // Let's make sure we don't have higher alignment requirements (e.g. if we add a u128 in the future). + const _: () = assert!( + mem::align_of::() <= 16, + "Graph alignment exceeds the 16-byte alignment guaranteed by Ruby's allocator" + ); + + unsafe { + pointer.cast::().write(Graph::new()); + } } -/// Frees a Graph through its pointer +/// Drops a Graph initialized by `rdx_graph_init` without freeing the underlying memory. +/// +/// # Safety +/// +/// `pointer` must point to a valid initialized Graph. #[unsafe(no_mangle)] -pub extern "C" fn rdx_graph_free(pointer: GraphPointer) { +pub unsafe extern "C" fn rdx_graph_drop(pointer: GraphPointer) { unsafe { - let _ = Box::from_raw(pointer.cast::()); + ptr::drop_in_place(pointer.cast::()); } } @@ -41,20 +62,16 @@ pub fn with_graph(pointer: GraphPointer, action: F) -> T where F: FnOnce(&Graph) -> T, { - let mut graph = unsafe { Box::from_raw(pointer.cast::()) }; - let result = action(&mut graph); - mem::forget(graph); - result + let graph = unsafe { &*pointer.cast::() }; + action(graph) } fn with_mut_graph(pointer: GraphPointer, action: F) -> T where F: FnOnce(&mut Graph) -> T, { - let mut graph = unsafe { Box::from_raw(pointer.cast::()) }; - let result = action(&mut graph); - mem::forget(graph); - result + let graph = unsafe { &mut *pointer.cast::() }; + action(graph) } /// Searches the graph using exact substring matching From c9fd983b91c572975311a43b9b36fd6dc6880c29 Mon Sep 17 00:00:00 2001 From: Alexander Momchilov Date: Thu, 11 Jun 2026 14:30:45 -0400 Subject: [PATCH 2/2] Update stale documentation --- ext/rubydex/graph.c | 6 +++-- rust/rubydex-sys/src/declaration_api.rs | 6 ++--- rust/rubydex-sys/src/definition_api.rs | 16 ++++++------- rust/rubydex-sys/src/diagnostic_api.rs | 2 +- rust/rubydex-sys/src/document_api.rs | 2 +- rust/rubydex-sys/src/graph_api.rs | 30 ++++++++++++------------- rust/rubydex-sys/src/reference_api.rs | 4 ++-- rust/rubydex-sys/src/signature_api.rs | 4 ++-- 8 files changed, 36 insertions(+), 34 deletions(-) diff --git a/ext/rubydex/graph.c b/ext/rubydex/graph.c index 0a88c0496..149263399 100644 --- a/ext/rubydex/graph.c +++ b/ext/rubydex/graph.c @@ -62,8 +62,10 @@ const rb_data_type_t graph_type = { .flags = RUBY_TYPED_FREE_IMMEDIATELY, }; -// Custom allocator for the Graph class. Calls into Rust to create a new `Arc>` that gets stored internally -// as a void pointer +// Custom allocator for the Graph class. +// Requests enough memory from the Ruby GC to fit a `Graph` Rust struct, then initializes it in-place. +// Only the `Graph` Rust struct itself is in the Ruby heap. Everything else it points to (e.g. all its hash maps' storage) +// are still normal Rust allocations from the Rust allocator, unknown to the Ruby GC. static VALUE rdxr_graph_alloc(VALUE klass) { void *graph; // Can't use `TypedData_Make_Struct`, because the Graph is a Rust type that isn't exposed directly to C. diff --git a/rust/rubydex-sys/src/declaration_api.rs b/rust/rubydex-sys/src/declaration_api.rs index 0990b4aa0..655ccb28e 100644 --- a/rust/rubydex-sys/src/declaration_api.rs +++ b/rust/rubydex-sys/src/declaration_api.rs @@ -243,7 +243,7 @@ pub unsafe extern "C" fn rdx_declaration_unqualified_name(pointer: GraphPointer, /// /// # Safety /// -/// - `pointer` must be a valid `GraphPointer` previously returned by this crate. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. /// - The returned pointer must be freed with `rdx_declaration_definitions_iter_free`. #[unsafe(no_mangle)] pub unsafe extern "C" fn rdx_declaration_definitions_iter_new( @@ -452,7 +452,7 @@ pub unsafe extern "C" fn rdx_constant_alias_target(pointer: GraphPointer, decl_i /// /// # Safety /// -/// - `pointer` must be a valid `GraphPointer` previously returned by this crate. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. /// - The returned pointer must be freed with `rdx_constant_references_iter_free`. #[unsafe(no_mangle)] pub unsafe extern "C" fn rdx_declaration_constant_references_iter_new( @@ -485,7 +485,7 @@ pub unsafe extern "C" fn rdx_declaration_constant_references_iter_new( /// /// # Safety /// -/// - `pointer` must be a valid `GraphPointer` previously returned by this crate. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. /// - The returned pointer must be freed with `rdx_method_references_iter_free`. #[unsafe(no_mangle)] pub unsafe extern "C" fn rdx_declaration_method_references_iter_new( diff --git a/rust/rubydex-sys/src/definition_api.rs b/rust/rubydex-sys/src/definition_api.rs index b5ae7aba6..73ce203b2 100644 --- a/rust/rubydex-sys/src/definition_api.rs +++ b/rust/rubydex-sys/src/definition_api.rs @@ -159,7 +159,7 @@ pub struct CommentArray { /// Caller must free the returned pointer with `rdx_definition_comments_free` and each inner string with `free_c_string` if needed. /// /// # Safety -/// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. /// - `definition_id` must be a valid definition id. /// /// # Panics @@ -232,7 +232,7 @@ pub unsafe extern "C" fn rdx_definition_comments_free(ptr: *mut CommentArray) { /// Caller must free the returned pointer with `rdx_location_free`. /// /// # Safety -/// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. /// - `definition_id` must be a valid definition id. /// /// # Panics @@ -256,7 +256,7 @@ pub unsafe extern "C" fn rdx_definition_location(pointer: GraphPointer, definiti /// with `free_c_declaration`. /// /// # Safety -/// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. /// - `definition_id` must be a valid definition id. /// /// # Panics @@ -280,7 +280,7 @@ pub unsafe extern "C" fn rdx_definition_declaration(pointer: GraphPointer, defin /// Caller must free the returned pointer with `free_u64`. /// /// # Safety -/// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. /// - `definition_id` must be a valid definition id. /// /// # Panics @@ -331,7 +331,7 @@ where /// Returns true if the definition is deprecated. /// /// # Safety -/// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. /// - `definition_id` must be a valid definition id. /// /// # Panics @@ -352,7 +352,7 @@ pub unsafe extern "C" fn rdx_definition_is_deprecated(pointer: GraphPointer, def /// Caller must free the returned pointer with `rdx_location_free`. /// /// # Safety -/// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. /// - `definition_id` must be a valid definition id. /// /// # Panics @@ -376,7 +376,7 @@ pub unsafe extern "C" fn rdx_definition_name_location(pointer: GraphPointer, def /// must free with `free_c_constant_reference`. /// /// # Safety -/// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. /// - `definition_id` must be a valid definition id for a class definition. /// /// # Panics @@ -462,7 +462,7 @@ fn map_mixin_kind(mixin: &Mixin) -> MixinKind { /// Returns NULL for definition types that do not support mixins. /// /// # Safety -/// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. /// - `definition_id` must be a valid definition id. /// /// # Panics diff --git a/rust/rubydex-sys/src/diagnostic_api.rs b/rust/rubydex-sys/src/diagnostic_api.rs index 39f9cbc27..0296e97aa 100644 --- a/rust/rubydex-sys/src/diagnostic_api.rs +++ b/rust/rubydex-sys/src/diagnostic_api.rs @@ -33,7 +33,7 @@ impl DiagnosticArray { /// /// # Safety /// -/// - `pointer` must be a valid `GraphPointer` previously returned by this crate. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. /// - The pointed graph must remain alive for the duration of the call. /// /// # Panics diff --git a/rust/rubydex-sys/src/document_api.rs b/rust/rubydex-sys/src/document_api.rs index 4db2fe4bd..136f0476e 100644 --- a/rust/rubydex-sys/src/document_api.rs +++ b/rust/rubydex-sys/src/document_api.rs @@ -69,7 +69,7 @@ pub unsafe extern "C" fn rdx_document_uri(pointer: GraphPointer, uri_id: u64) -> /// /// # Safety /// -/// - `pointer` must be a valid `GraphPointer` previously returned by this crate. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. /// - The returned pointer must be freed with `rdx_document_definitions_iter_free`. #[unsafe(no_mangle)] pub unsafe extern "C" fn rdx_document_definitions_iter_new(pointer: GraphPointer, uri_id: u64) -> *mut DefinitionsIter { diff --git a/rust/rubydex-sys/src/graph_api.rs b/rust/rubydex-sys/src/graph_api.rs index b08fdb367..116bf3a5b 100644 --- a/rust/rubydex-sys/src/graph_api.rs +++ b/rust/rubydex-sys/src/graph_api.rs @@ -182,7 +182,7 @@ pub unsafe extern "C" fn rdx_graph_resolve_constant( /// /// # Safety /// -/// - `pointer` must be a valid `GraphPointer` previously returned by this crate. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. /// - `paths` must be an array of `count` valid, null-terminated UTF-8 strings. #[unsafe(no_mangle)] pub unsafe extern "C" fn rdx_graph_exclude_paths(pointer: GraphPointer, paths: *const *const c_char, count: usize) { @@ -196,7 +196,7 @@ pub unsafe extern "C" fn rdx_graph_exclude_paths(pointer: GraphPointer, paths: * /// /// # Safety /// -/// - `pointer` must be a valid `GraphPointer` previously returned by this crate. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. /// - `out_count` must be a valid, writable pointer. #[unsafe(no_mangle)] pub unsafe extern "C" fn rdx_graph_excluded_paths( @@ -335,7 +335,7 @@ pub extern "C" fn rdx_graph_resolve(pointer: GraphPointer) { /// /// # Safety /// -/// - `pointer` must be a valid `GraphPointer` previously returned by this crate. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. /// - `out_error_count` must be a valid, writable pointer. #[unsafe(no_mangle)] pub unsafe extern "C" fn rdx_check_integrity( @@ -395,7 +395,7 @@ pub unsafe extern "C" fn rdx_graph_set_encoding(pointer: GraphPointer, encoding_ /// /// # Safety /// -/// - `pointer` must be a valid `GraphPointer` previously returned by this crate. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. /// - The returned pointer must be freed with `rdx_graph_declarations_iter_free`. #[unsafe(no_mangle)] pub unsafe extern "C" fn rdx_graph_declarations_iter_new(pointer: GraphPointer) -> *mut DeclarationsIter { @@ -416,7 +416,7 @@ pub unsafe extern "C" fn rdx_graph_declarations_iter_new(pointer: GraphPointer) /// /// # Safety /// -/// - `pointer` must be a valid `GraphPointer` previously returned by this crate. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. /// - The returned pointer must be freed with `rdx_graph_documents_iter_free`. #[unsafe(no_mangle)] pub unsafe extern "C" fn rdx_graph_documents_iter_new(pointer: GraphPointer) -> *mut DocumentsIter { @@ -458,7 +458,7 @@ pub unsafe extern "C" fn rdx_graph_get_declaration(pointer: GraphPointer, name: /// Creates a new iterator over constant references by snapshotting the current set of IDs. /// /// # Safety -/// - `pointer` must be a valid `GraphPointer` previously returned by this crate. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. #[unsafe(no_mangle)] pub unsafe extern "C" fn rdx_graph_constant_references_iter_new(pointer: GraphPointer) -> *mut ConstantReferencesIter { with_graph(pointer, |graph| { @@ -489,7 +489,7 @@ pub unsafe extern "C" fn rdx_graph_constant_references_iter_new(pointer: GraphPo /// Creates a new iterator over method references by snapshotting the current set of IDs. /// /// # Safety -/// - `pointer` must be a valid `GraphPointer` previously returned by this crate. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. #[unsafe(no_mangle)] pub unsafe extern "C" fn rdx_graph_method_references_iter_new(pointer: GraphPointer) -> *mut MethodReferencesIter { with_graph(pointer, |graph| { @@ -508,7 +508,7 @@ pub unsafe extern "C" fn rdx_graph_method_references_iter_new(pointer: GraphPoin /// Caller must free with the returned pointer. /// /// # Safety -/// - `pointer` must be a valid `GraphPointer` previously returned by this crate. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. /// - `require_path` must be a valid, null-terminated UTF-8 string. /// - `load_paths` must be an array of `load_paths_count` valid, null-terminated UTF-8 strings. #[unsafe(no_mangle)] @@ -538,7 +538,7 @@ pub unsafe extern "C" fn rdx_resolve_require_path( /// Caller must free with `free_c_string_array`. /// /// # Safety -/// - `pointer` must be a valid `GraphPointer` previously returned by this crate. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. /// - `load_path` must be an array of `load_path_count` valid, null-terminated UTF-8 strings. /// - `out_count` must be a valid, writable pointer. #[unsafe(no_mangle)] @@ -585,7 +585,7 @@ pub enum IndexSourceResult { /// /// # Safety /// -/// - `pointer` must be a valid `GraphPointer` previously returned by this crate. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. /// - `uri` must be a valid, null-terminated UTF-8 string. /// - `language_id` must be either null or a valid, null-terminated UTF-8 string. /// - `source` must point to a valid UTF-8 byte buffer of at least `source_len` bytes. @@ -816,7 +816,7 @@ fn run_and_finalize_completion( /// /// # Safety /// -/// - `pointer` must be a valid `GraphPointer` previously returned by this crate. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. /// - `nesting` must point to `nesting_count` valid, null-terminated UTF-8 strings. /// - `self_receiver` must be null or a valid, null-terminated UTF-8 string. When non-null, it /// overrides the self-type (e.g., `"Foo::"` for completion inside `def Foo.bar`), while @@ -853,7 +853,7 @@ pub unsafe extern "C" fn rdx_graph_complete_expression( /// /// # Safety /// -/// - `pointer` must be a valid `GraphPointer` previously returned by this crate. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. /// - `name` must be a valid, null-terminated UTF-8 string (FQN of the namespace). /// - `self_receiver` must be null or a valid, null-terminated UTF-8 string. When non-null, it /// is the caller's runtime self type (e.g., for filtering `private_class_method` visibility). @@ -887,7 +887,7 @@ pub unsafe extern "C" fn rdx_graph_complete_namespace_access( /// /// # Safety /// -/// - `pointer` must be a valid `GraphPointer` previously returned by this crate. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. /// - `name` must be a valid, null-terminated UTF-8 string (FQN of the receiver). /// - `self_receiver` must be null or a valid, null-terminated UTF-8 string. When non-null, it /// is the caller's runtime self type, used for MRI-style visibility checks. @@ -921,7 +921,7 @@ pub unsafe extern "C" fn rdx_graph_complete_method_call( /// /// # Safety /// -/// - `pointer` must be a valid `GraphPointer` previously returned by this crate. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. /// - `name` must be a valid, null-terminated UTF-8 string (FQN of the method). /// - `nesting` must point to `nesting_count` valid, null-terminated UTF-8 strings. /// - `self_receiver` must be null or a valid, null-terminated UTF-8 string. See @@ -1018,7 +1018,7 @@ pub enum CVisibility { /// /// # Safety /// -/// - `pointer` must be a valid `GraphPointer` previously returned by this crate. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. #[unsafe(no_mangle)] pub unsafe extern "C" fn rdx_graph_visibility(pointer: GraphPointer, declaration_id: u64) -> *const CVisibility { with_graph(pointer, |graph| { diff --git a/rust/rubydex-sys/src/reference_api.rs b/rust/rubydex-sys/src/reference_api.rs index 9bbf4c908..ba8a5af5d 100644 --- a/rust/rubydex-sys/src/reference_api.rs +++ b/rust/rubydex-sys/src/reference_api.rs @@ -175,7 +175,7 @@ pub unsafe extern "C" fn rdx_method_reference_name(pointer: GraphPointer, refere /// /// # Safety /// -/// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. /// - `reference_id` must be a valid reference id. /// /// # Panics @@ -268,7 +268,7 @@ pub unsafe extern "C" fn rdx_method_reference_receiver_declaration( /// /// # Safety /// -/// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. /// - `reference_id` must be a valid reference id. /// /// # Panics diff --git a/rust/rubydex-sys/src/signature_api.rs b/rust/rubydex-sys/src/signature_api.rs index dc1dba2bb..d4d048355 100644 --- a/rust/rubydex-sys/src/signature_api.rs +++ b/rust/rubydex-sys/src/signature_api.rs @@ -65,7 +65,7 @@ pub struct SignatureArray { /// Caller must free the returned pointer with `rdx_definition_signatures_free`. /// /// # Safety -/// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. /// - `definition_id` must be a valid definition id. /// /// # Panics @@ -132,7 +132,7 @@ fn collect_method_signatures(graph: &Graph, method_def: &MethodDefinition) -> Ve /// are silently ignored. /// /// # Safety -/// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`. +/// - `pointer` must be a valid pointer previously initialized by `rdx_graph_init`. /// - `definition_id` must be a valid definition id. /// /// # Panics