rubydex 0.4.3 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 427fb322be8d5ed974dbd0ea8a1c70b90457b95955d3ef5de1e0bba54b48ec16
4
- data.tar.gz: 5dcf6a17e96ec38f2de7351b36b0d6e04c1e558645487c3cb38c5d00be6be003
3
+ metadata.gz: 196878527af0452d4afb518e0520ed26ccf7370fc6755954d70038afa6e0f66e
4
+ data.tar.gz: 864a4ac367601c64a465946115b732da341971b2a7c9d06f96ce8b3fae983354
5
5
  SHA512:
6
- metadata.gz: 7c7d78c466ac7e1b5312ac5e8bc74dfc55ac111b7f53eb119251def4f11d32d24051cdff1ebe15cc67d7557f6a9de7051ac98bab7f174d9bb07f76d210bc8abc
7
- data.tar.gz: 2bb52d1af8cecc3e57905fe10173d5758d4b91c47ccdb48253780ba3e422fee57a5d73072351a344288bb4c2761d13c95ba279ce91e31da6dd9997f3f472cdc2
6
+ metadata.gz: 30ef223c97026abc1cb3f1a8c84048f87889dbb96aa01fc81fe37a9db52109cbb774ba492635ec09067050ab7a33dbd2b7f40aac5512dc81367d7f0009c036ee
7
+ data.tar.gz: 20e4b61e4790603a8589419404d85011ef0f9342b4e0cc65f056b19fdadc5a6d881a37f3e49cd9328c072bc64cc6e988350e0372da32243632d5ae2a1de2fcbf
@@ -2848,10 +2848,10 @@ SOFTWARE.
2848
2848
  <ul class="license-used-by">
2849
2849
  <li><a
2850
2850
  href=" https://github.com/Shopify/rubydex ">rubydex
2851
- 0.4.3</a></li>
2851
+ 0.4.4</a></li>
2852
2852
  <li><a
2853
2853
  href=" https://github.com/Shopify/rubydex ">rubydex-sys
2854
- 0.4.3</a></li>
2854
+ 0.4.4</a></li>
2855
2855
  <li><a
2856
2856
  href=" https://github.com/DimaKudosh/difflib ">difflib
2857
2857
  0.4.0</a></li>
@@ -60,9 +60,7 @@ VALUE rdxi_declaration_class_for_kind(CDeclarationKind kind) {
60
60
  static VALUE rdxr_declaration_name(VALUE self) {
61
61
  HandleData *data;
62
62
  void *graph = rdxi_graph_from_handle(self, &data);
63
- const char *name = rdx_declaration_name(graph, data->id);
64
-
65
- return rdxi_owned_c_string_to_ruby(name);
63
+ return rdxi_owned_string_to_ruby(rdx_declaration_name(graph, data->id));
66
64
  }
67
65
 
68
66
  /*
@@ -74,9 +72,7 @@ static VALUE rdxr_declaration_name(VALUE self) {
74
72
  static VALUE rdxr_declaration_unqualified_name(VALUE self) {
75
73
  HandleData *data;
76
74
  void *graph = rdxi_graph_from_handle(self, &data);
77
- const char *name = rdx_declaration_unqualified_name(graph, data->id);
78
-
79
- return rdxi_owned_c_string_to_ruby(name);
75
+ return rdxi_owned_string_to_ruby(rdx_declaration_unqualified_name(graph, data->id));
80
76
  }
81
77
 
82
78
  // Body function for rb_ensure in Declaration#definitions
@@ -132,7 +132,7 @@ static VALUE rdxr_definition_comments(VALUE self) {
132
132
  for (size_t i = 0; i < arr->len; i++) {
133
133
  CommentEntry entry = arr->items[i];
134
134
 
135
- VALUE string = rb_utf8_str_new_cstr(entry.string);
135
+ VALUE string = rb_utf8_str_new(entry.string.data, (long)entry.string.length);
136
136
 
137
137
  Location *loc = entry.location;
138
138
  VALUE location = rdxi_build_location_value(loc);
@@ -160,8 +160,7 @@ static VALUE rdxr_definition_name(VALUE self) {
160
160
  HandleData *data;
161
161
  void *graph = rdxi_graph_from_handle(self, &data);
162
162
 
163
- const char *name = rdx_definition_name(graph, data->id);
164
- return rdxi_owned_c_string_to_ruby(name);
163
+ return rdxi_owned_string_to_ruby(rdx_definition_name(graph, data->id));
165
164
  }
166
165
 
167
166
  /*
data/ext/rubydex/graph.c CHANGED
@@ -679,7 +679,8 @@ static VALUE rdxr_graph_diagnostics(VALUE self) {
679
679
  VALUE diagnostics = rb_ary_new_capa((long)array->len);
680
680
  for (size_t i = 0; i < array->len; i++) {
681
681
  DiagnosticEntry entry = array->items[i];
682
- VALUE message = entry.message == NULL ? Qnil : rb_utf8_str_new_cstr(entry.message);
682
+ VALUE message =
683
+ entry.message.data == NULL ? Qnil : rb_utf8_str_new(entry.message.data, (long)entry.message.length);
683
684
  VALUE rule = rdxi_rule_class_from_name(entry.rule.name, entry.rule.name_length);
684
685
  VALUE location = rdxi_build_location_value(entry.location);
685
686
 
data/ext/rubydex/query.c CHANGED
@@ -188,7 +188,9 @@ static VALUE cypher_cell_to_value(VALUE graph_obj, const struct CCell *cell) {
188
188
  case CCellTag_Int:
189
189
  return LL2NUM(cell->payload.int_val);
190
190
  case CCellTag_Str:
191
- return cell->payload.str_val == NULL ? Qnil : rb_utf8_str_new_cstr(cell->payload.str_val);
191
+ return cell->payload.str_val.data == NULL
192
+ ? Qnil
193
+ : rb_utf8_str_new(cell->payload.str_val.data, (long)cell->payload.str_val.length);
192
194
  case CCellTag_List: {
193
195
  VALUE array = rb_ary_new_capa((long)cell->payload.list.len);
194
196
  for (size_t i = 0; i < cell->payload.list.len; i++) {
@@ -86,11 +86,11 @@ static VALUE rdxr_method_reference_name(VALUE self) {
86
86
  HandleData *data;
87
87
  void *graph = rdxi_graph_from_handle(self, &data);
88
88
 
89
- const char *name = rdx_method_reference_name(graph, data->id);
90
- if (name == NULL) {
89
+ OwnedString name = rdx_method_reference_name(graph, data->id);
90
+ if (name.data == NULL) {
91
91
  rb_raise(rb_eRuntimeError, "Method reference must exist for a valid id");
92
92
  }
93
- return rdxi_owned_c_string_to_ruby(name);
93
+ return rdxi_owned_string_to_ruby(name);
94
94
  }
95
95
 
96
96
  /*
data/ext/rubydex/utils.c CHANGED
@@ -59,6 +59,19 @@ VALUE rdxi_owned_c_string_to_ruby(const char *string) {
59
59
  return value;
60
60
  }
61
61
 
62
+ // Convert a Rust-owned string with an explicit length to a Ruby string and release it with free_owned_string.
63
+ // Returns nil when the Rust side returned a null string.
64
+ VALUE rdxi_owned_string_to_ruby(OwnedString string) {
65
+ if (string.data == NULL) {
66
+ return Qnil;
67
+ }
68
+
69
+ VALUE value = rb_utf8_str_new(string.data, (long)string.length);
70
+ free_owned_string(string);
71
+
72
+ return value;
73
+ }
74
+
62
75
  // Coerce an optional String, Symbol, or nil to a C string, defaulting to `default_value` for nil.
63
76
  const char *rdxi_symbol_or_string_cstr(VALUE value, const char *default_value) {
64
77
  if (NIL_P(value)) {
data/ext/rubydex/utils.h CHANGED
@@ -2,6 +2,7 @@
2
2
  #define RUBYDEX_UTILS_H
3
3
 
4
4
  #include "ruby.h"
5
+ #include "rustbindings.h"
5
6
 
6
7
  // Convert a Ruby array of strings into a double char pointer so that we can pass that to Rust.
7
8
  // This copies the data so it must be freed with rdxi_free_str_array
@@ -17,6 +18,10 @@ void rdxi_check_array_of_strings(VALUE array);
17
18
  // Returns nil when the Rust side returned NULL.
18
19
  VALUE rdxi_owned_c_string_to_ruby(const char *string);
19
20
 
21
+ // Convert a Rust-owned string with an explicit length to a Ruby string and release it with free_owned_string. Unlike
22
+ // a C string, it may contain NUL bytes. Returns nil when the Rust side returned a null string.
23
+ VALUE rdxi_owned_string_to_ruby(OwnedString string);
24
+
20
25
  // Coerce an optional String, Symbol, or nil to a C string, returning `default_value` when the value
21
26
  // is nil. A non-nil value must be a String or Symbol, otherwise a `TypeError` is raised. The
22
27
  // returned pointer is owned by the Ruby VALUE and is only valid while it stays live.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rubydex
4
- VERSION = "0.4.3"
4
+ VERSION = "0.4.4"
5
5
  end
data/rust/Cargo.lock CHANGED
@@ -760,7 +760,7 @@ dependencies = [
760
760
 
761
761
  [[package]]
762
762
  name = "rubydex"
763
- version = "0.4.3"
763
+ version = "0.4.4"
764
764
  dependencies = [
765
765
  "assert_cmd",
766
766
  "bitflags",
@@ -789,7 +789,7 @@ dependencies = [
789
789
 
790
790
  [[package]]
791
791
  name = "rubydex-sys"
792
- version = "0.4.3"
792
+ version = "0.4.4"
793
793
  dependencies = [
794
794
  "cbindgen",
795
795
  "libc",
data/rust/Cargo.toml CHANGED
@@ -7,7 +7,8 @@ members = [
7
7
  resolver = "3"
8
8
 
9
9
  [workspace.package]
10
- version = "0.4.3"
10
+ version = "0.4.4"
11
+ rust-version = "1.99.1"
11
12
 
12
13
  [profile.release]
13
14
  lto = true
@@ -28,4 +29,4 @@ complexity = "deny"
28
29
  style = "deny"
29
30
  pedantic = "warn"
30
31
 
31
- ptr-as-ptr = { level = "deny", priority = 1 }
32
+ ptr_as_ptr = { level = "deny", priority = 1 }
@@ -2,7 +2,7 @@
2
2
  name = "rubydex"
3
3
  version.workspace = true
4
4
  edition = "2024"
5
- rust-version = "1.89.0"
5
+ rust-version.workspace = true
6
6
  license = "MIT"
7
7
  description = "High-performance Ruby code indexing and static analysis library."
8
8
  homepage = "https://github.com/Shopify/rubydex"
@@ -2,7 +2,7 @@
2
2
  name = "rubydex-sys"
3
3
  version.workspace = true
4
4
  edition = "2024"
5
- rust-version = "1.89.0"
5
+ rust-version.workspace = true
6
6
  license = "MIT"
7
7
  description = "C FFI bindings for the Rubydex Ruby code indexing engine."
8
8
  homepage = "https://github.com/Shopify/rubydex"
@@ -15,7 +15,7 @@ categories = ["development-tools", "external-ffi-bindings"]
15
15
  crate-type = ["cdylib", "staticlib"]
16
16
 
17
17
  [dependencies]
18
- rubydex = { version = "=0.4.3", path = "../rubydex" }
18
+ rubydex = { version = "=0.4.4", path = "../rubydex" }
19
19
  libc = "0.2.174"
20
20
  url = "2.5.4"
21
21
  line-index = "0.1.2"
@@ -5,11 +5,13 @@ use crate::declaration_api::CDeclaration;
5
5
  use crate::definition_api::map_definition_to_kind;
6
6
  use crate::graph_api::{GraphPointer, with_graph};
7
7
  use crate::utils;
8
+ use crate::utils::OwnedString;
8
9
  use libc::{c_char, c_void};
9
10
  use rubydex::model::graph::Graph;
10
11
  use rubydex::query::cypher::schema::NodeRef;
11
12
  use rubydex::query::cypher::{self, CypherValue, OutputFormat};
12
13
  use std::ffi::CString;
14
+ use std::mem::ManuallyDrop;
13
15
  use std::ptr;
14
16
 
15
17
  /// Which layer of the Cypher pipeline rejected a call, so callers can raise a matching error class.
@@ -201,14 +203,13 @@ pub struct CMap {
201
203
  /// Payload of a `CCell`. The active field is selected by the cell's `tag`; reading any other field
202
204
  /// is undefined. `Null` carries no payload.
203
205
  #[repr(C)]
204
- #[derive(Clone, Copy)]
205
206
  pub union CCellPayload {
206
207
  /// `Bool`.
207
208
  pub bool_val: bool,
208
209
  /// `Int`.
209
210
  pub int_val: i64,
210
- /// `Str`: owned C string (freed by `rdx_rows_iter_free`).
211
- pub str_val: *const c_char,
211
+ /// `Str`: owned string (freed by `rdx_rows_iter_free`). A union never drops its fields, so `free_cell` drops it.
212
+ pub str_val: ManuallyDrop<OwnedString>,
212
213
  /// `Node`.
213
214
  pub node: CNode,
214
215
  /// `List`.
@@ -306,7 +307,7 @@ fn build_cell(graph: &Graph, value: &CypherValue) -> Result<CCell, String> {
306
307
  CypherValue::Str(s) => Ok(CCell::new(
307
308
  CCellTag::Str,
308
309
  CCellPayload {
309
- str_val: utils::cstring_raw(s),
310
+ str_val: ManuallyDrop::new(OwnedString::new(s)),
310
311
  },
311
312
  )),
312
313
  CypherValue::List(items) => {
@@ -715,10 +716,9 @@ unsafe fn free_cell(cell: &CCell) {
715
716
  match cell.tag {
716
717
  // SAFETY: the tag selects the active union field.
717
718
  CCellTag::Str => {
718
- let str_val = unsafe { cell.payload.str_val };
719
- if !str_val.is_null() {
720
- let _ = unsafe { CString::from_raw(str_val.cast_mut()) };
721
- }
719
+ // The cell is not used after this, so moving the string out of it is sound
720
+ let string = unsafe { ptr::read(&raw const cell.payload.str_val) };
721
+ drop(ManuallyDrop::into_inner(string));
722
722
  }
723
723
  // SAFETY: the tag selects the active union field.
724
724
  CCellTag::List => {
@@ -2,13 +2,13 @@
2
2
 
3
3
  use libc::c_char;
4
4
  use rubydex::model::declaration::{Ancestor, Declaration, Namespace};
5
- use std::ffi::CString;
6
5
  use std::ptr;
7
6
 
8
7
  use crate::definition_api::{DefinitionsIter, rdx_definitions_iter_new_from_ids};
9
8
  use crate::graph_api::{GraphPointer, with_graph};
10
9
  use crate::reference_api::{CConstantReference, CMethodReference, ConstantReferencesIter, MethodReferencesIter};
11
10
  use crate::utils;
11
+ use crate::utils::OwnedString;
12
12
  use rubydex::model::ids::{DeclarationId, StringId, declaration_id_from_lookup_name};
13
13
 
14
14
  #[repr(C)]
@@ -119,24 +119,20 @@ pub unsafe extern "C" fn rdx_graph_declarations_iter_free(iter: *mut Declaration
119
119
  unsafe { DeclarationsIter::free(iter) }
120
120
  }
121
121
 
122
- /// Returns the UTF-8 name string for a declaration id.
123
- /// Caller must free with `free_c_string`.
122
+ /// Returns the UTF-8 name string for a declaration id, or a null string if the declaration cannot be found.
123
+ /// Caller must free with `free_owned_string`.
124
124
  ///
125
125
  /// # Safety
126
126
  ///
127
127
  /// Assumes pointer is valid.
128
- ///
129
- /// # Panics
130
- ///
131
- /// This function will panic if the name pointer is invalid.
132
128
  #[unsafe(no_mangle)]
133
- pub unsafe extern "C" fn rdx_declaration_name(pointer: GraphPointer, name_id: u64) -> *const c_char {
129
+ pub unsafe extern "C" fn rdx_declaration_name(pointer: GraphPointer, name_id: u64) -> OwnedString {
134
130
  with_graph(pointer, |graph| {
135
131
  let name_id = DeclarationId::new(name_id);
136
132
  if let Some(decl) = graph.declarations().get(&name_id) {
137
- CString::new(decl.name()).unwrap().into_raw().cast_const()
133
+ OwnedString::new(decl.name())
138
134
  } else {
139
- ptr::null()
135
+ OwnedString::null()
140
136
  }
141
137
  })
142
138
  }
@@ -213,24 +209,20 @@ pub unsafe extern "C" fn rdx_declaration_find_member(
213
209
  })
214
210
  }
215
211
 
216
- /// Returns the UTF-8 unqualified name string for a declaration id.
217
- /// Caller must free with `free_c_string`.
212
+ /// Returns the UTF-8 unqualified name string for a declaration id, or a null string if the declaration cannot be
213
+ /// found. Caller must free with `free_owned_string`.
218
214
  ///
219
215
  /// # Safety
220
216
  ///
221
217
  /// Assumes pointer is valid.
222
- ///
223
- /// # Panics
224
- ///
225
- /// This function will panic if the name pointer is invalid.
226
218
  #[unsafe(no_mangle)]
227
- pub unsafe extern "C" fn rdx_declaration_unqualified_name(pointer: GraphPointer, name_id: u64) -> *const c_char {
219
+ pub unsafe extern "C" fn rdx_declaration_unqualified_name(pointer: GraphPointer, name_id: u64) -> OwnedString {
228
220
  with_graph(pointer, |graph| {
229
221
  let name_id = DeclarationId::new(name_id);
230
222
  if let Some(decl) = graph.declarations().get(&name_id) {
231
- CString::new(decl.unqualified_name()).unwrap().into_raw().cast_const()
223
+ OwnedString::new(&decl.unqualified_name())
232
224
  } else {
233
- ptr::null()
225
+ OwnedString::null()
234
226
  }
235
227
  })
236
228
  }
@@ -4,6 +4,7 @@ use crate::declaration_api::CDeclaration;
4
4
  use crate::graph_api::{GraphPointer, with_graph};
5
5
  use crate::location_api::{Location, create_location_for_uri_and_offset};
6
6
  use crate::reference_api::CConstantReference;
7
+ use crate::utils::OwnedString;
7
8
  use libc::c_char;
8
9
  use rubydex::model::definitions::{Definition, Mixin};
9
10
  use rubydex::model::graph::Graph;
@@ -84,30 +85,26 @@ pub unsafe extern "C" fn rdx_definition_kind(pointer: GraphPointer, definition_i
84
85
  })
85
86
  }
86
87
 
87
- /// Returns the UTF-8 unqualified name string for a definition id.
88
- /// Caller must free with `free_c_string`.
88
+ /// Returns the UTF-8 unqualified name string for a definition id, or a null string if the definition cannot be found.
89
+ /// Caller must free with `free_owned_string`.
89
90
  ///
90
91
  /// # Safety
91
92
  ///
92
93
  /// Assumes pointer is valid.
93
- ///
94
- /// # Panics
95
- ///
96
- /// This function will panic if the definition cannot be found.
97
94
  #[unsafe(no_mangle)]
98
- pub unsafe extern "C" fn rdx_definition_name(pointer: GraphPointer, definition_id: u64) -> *const c_char {
95
+ pub unsafe extern "C" fn rdx_definition_name(pointer: GraphPointer, definition_id: u64) -> OwnedString {
99
96
  with_graph(pointer, |graph| {
100
97
  let def_id = DefinitionId::new(definition_id);
101
98
  if let Some(defn) = graph.definitions().get(&def_id) {
102
99
  let string_id = graph.definition_string_id(defn);
103
100
 
104
101
  if let Some(name) = graph.strings().get(&string_id) {
105
- CString::new(name.as_str()).unwrap().into_raw().cast_const()
102
+ OwnedString::new(name.as_str())
106
103
  } else {
107
- ptr::null()
104
+ OwnedString::null()
108
105
  }
109
106
  } else {
110
- ptr::null()
107
+ OwnedString::null()
111
108
  }
112
109
  })
113
110
  }
@@ -212,7 +209,7 @@ pub unsafe extern "C" fn rdx_definitions_iter_free(iter: *mut DefinitionsIter) {
212
209
  /// C-compatible struct representing a single comment with its string and location
213
210
  #[repr(C)]
214
211
  pub struct CommentEntry {
215
- pub string: *const c_char,
212
+ pub string: OwnedString,
216
213
  pub location: *mut Location,
217
214
  }
218
215
 
@@ -248,7 +245,7 @@ pub unsafe extern "C" fn rdx_definition_comments(pointer: GraphPointer, definiti
248
245
  .comments()
249
246
  .iter()
250
247
  .map(|c| CommentEntry {
251
- string: CString::new(c.string()).unwrap().into_raw().cast_const(),
248
+ string: OwnedString::new(c.string()),
252
249
  location: create_location_for_uri_and_offset(graph, document, c.offset()),
253
250
  })
254
251
  .collect::<Vec<CommentEntry>>()
@@ -282,17 +279,13 @@ pub unsafe extern "C" fn rdx_definition_comments_free(ptr: *mut CommentArray) {
282
279
  let mut boxed_slice: Box<[CommentEntry]> = unsafe { Box::from_raw(slice_ptr) };
283
280
 
284
281
  for item in &mut boxed_slice {
285
- if !item.string.is_null() {
286
- // Free the CString allocated for the comment string
287
- let _ = unsafe { CString::from_raw(item.string.cast_mut()) };
288
- }
289
282
  if !item.location.is_null() {
290
283
  unsafe { crate::location_api::rdx_location_free(item.location) };
291
284
  item.location = ptr::null_mut();
292
285
  }
293
286
  }
294
287
 
295
- // boxed_slice is dropped here, freeing the items buffer
288
+ // boxed_slice is dropped here, freeing the items buffer and each item's string
296
289
  }
297
290
  // arr is dropped here
298
291
  }
@@ -2,9 +2,10 @@
2
2
 
3
3
  use crate::graph_api::{GraphPointer, with_graph};
4
4
  use crate::location_api::{Location, create_location_for_uri_and_offset};
5
+ use crate::utils::OwnedString;
5
6
  use libc::c_char;
6
7
  use rubydex::diagnostic::{Rule, Severity};
7
- use std::{ffi::CString, mem, ptr};
8
+ use std::{mem, ptr};
8
9
 
9
10
  /// C-compatible enum representing diagnostic severity levels.
10
11
  #[repr(C)]
@@ -49,7 +50,7 @@ impl From<Rule> for CRule {
49
50
  #[repr(C)]
50
51
  pub struct DiagnosticEntry {
51
52
  pub rule: CRule,
52
- pub message: *const c_char,
53
+ pub message: OwnedString,
53
54
  pub location: *mut Location,
54
55
  }
55
56
 
@@ -91,7 +92,7 @@ pub unsafe extern "C" fn rdx_graph_diagnostics(pointer: GraphPointer) -> *mut Di
91
92
 
92
93
  DiagnosticEntry {
93
94
  rule: CRule::from(*diagnostic.rule()),
94
- message: CString::new(diagnostic.message()).unwrap().into_raw().cast_const(),
95
+ message: OwnedString::new(diagnostic.message()),
95
96
  location,
96
97
  }
97
98
  })
@@ -119,15 +120,12 @@ pub unsafe extern "C" fn rdx_diagnostics_free(ptr: *mut DiagnosticArray) {
119
120
  let mut boxed_slice: Box<[DiagnosticEntry]> = unsafe { Box::from_raw(slice_ptr) };
120
121
 
121
122
  for entry in &mut *boxed_slice {
122
- if !entry.message.is_null() {
123
- let _ = unsafe { CString::from_raw(entry.message.cast_mut()) };
124
- }
125
123
  if !entry.location.is_null() {
126
124
  unsafe { crate::location_api::rdx_location_free(entry.location) };
127
125
  entry.location = ptr::null_mut();
128
126
  }
129
127
  }
130
- // boxed_slice drops here, releasing the buffer
128
+ // boxed_slice drops here, releasing the buffer and each entry's message
131
129
  }
132
130
  // array drops here
133
131
  }
@@ -6,6 +6,7 @@ use std::ptr;
6
6
  use crate::declaration_api::CDeclaration;
7
7
  use crate::graph_api::{GraphPointer, with_graph};
8
8
  use crate::location_api::{Location, create_location_for_uri_and_offset};
9
+ use crate::utils::OwnedString;
9
10
  use libc::c_char;
10
11
  use rubydex::model::graph::Graph;
11
12
  use rubydex::model::ids::{ConstantReferenceId, MethodReferenceId};
@@ -148,8 +149,8 @@ pub unsafe extern "C" fn rdx_constant_reference_name(pointer: GraphPointer, refe
148
149
  })
149
150
  }
150
151
 
151
- /// Returns the UTF-8 name string for a method reference id, or NULL if the reference cannot be found.
152
- /// Caller must free with `free_c_string`.
152
+ /// Returns the UTF-8 name string for a method reference id, or a null string if the reference cannot be found.
153
+ /// Caller must free with `free_owned_string`.
153
154
  ///
154
155
  /// # Safety
155
156
  ///
@@ -159,18 +160,14 @@ pub unsafe extern "C" fn rdx_constant_reference_name(pointer: GraphPointer, refe
159
160
  ///
160
161
  /// This function will panic if the reference's name cannot be found.
161
162
  #[unsafe(no_mangle)]
162
- pub unsafe extern "C" fn rdx_method_reference_name(pointer: GraphPointer, reference_id: u64) -> *const c_char {
163
+ pub unsafe extern "C" fn rdx_method_reference_name(pointer: GraphPointer, reference_id: u64) -> OwnedString {
163
164
  with_graph(pointer, |graph| {
164
165
  let ref_id = MethodReferenceId::new(reference_id);
165
166
  let Some(reference) = graph.method_references().get(&ref_id) else {
166
- return ptr::null();
167
+ return OwnedString::null();
167
168
  };
168
- let name = graph
169
- .strings()
170
- .get(reference.str())
171
- .expect("Name ID should exist")
172
- .to_string();
173
- CString::new(name).unwrap().into_raw().cast_const()
169
+ let name = graph.strings().get(reference.str()).expect("Name ID should exist");
170
+ OwnedString::new(name.as_str())
174
171
  })
175
172
  }
176
173
 
@@ -1,6 +1,7 @@
1
1
  use libc::{c_char, size_t};
2
2
  use std::ffi::{CStr, CString};
3
3
  use std::path::Path;
4
+ use std::ptr;
4
5
  use std::slice;
5
6
  use std::str::Utf8Error;
6
7
 
@@ -42,6 +43,54 @@ pub extern "C" fn free_c_string(ptr: *const c_char) {
42
43
  }
43
44
  }
44
45
 
46
+ /// C-compatible owned UTF-8 string with an explicit length. Unlike a C string, it is not NUL-terminated and may
47
+ /// contain NUL bytes, which source code can put into names, comments and diagnostic messages. A null `data` means
48
+ /// there is no string. Dropping it frees the bytes
49
+ #[repr(C)]
50
+ pub struct OwnedString {
51
+ pub data: *const c_char,
52
+ pub length: usize,
53
+ }
54
+
55
+ impl OwnedString {
56
+ #[must_use]
57
+ pub fn new(value: &str) -> Self {
58
+ let bytes: Box<[u8]> = value.as_bytes().into();
59
+ Self {
60
+ length: bytes.len(),
61
+ data: Box::into_raw(bytes).cast::<c_char>().cast_const(),
62
+ }
63
+ }
64
+
65
+ #[must_use]
66
+ pub fn null() -> Self {
67
+ Self {
68
+ data: ptr::null(),
69
+ length: 0,
70
+ }
71
+ }
72
+ }
73
+
74
+ impl Drop for OwnedString {
75
+ fn drop(&mut self) {
76
+ if !self.data.is_null() {
77
+ let bytes = ptr::slice_from_raw_parts_mut(self.data.cast_mut().cast::<u8>(), self.length);
78
+ // SAFETY: a non-null `data` always comes from `Box::into_raw` in `OwnedString::new`, with `length` bytes
79
+ let _ = unsafe { Box::from_raw(bytes) };
80
+ }
81
+ }
82
+ }
83
+
84
+ /// Frees an `OwnedString` allocated on the Rust side
85
+ ///
86
+ /// # Safety
87
+ ///
88
+ /// - `string` must have been returned by a rubydex function and must not be used after being freed
89
+ #[unsafe(no_mangle)]
90
+ pub unsafe extern "C" fn free_owned_string(string: OwnedString) {
91
+ drop(string);
92
+ }
93
+
45
94
  /// Frees a boxed u64 allocated on the Rust side
46
95
  #[unsafe(no_mangle)]
47
96
  pub extern "C" fn free_u64(ptr: *const u64) {
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubydex
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.3
4
+ version: 0.4.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify