pf2 0.4.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d576aec57e600cc77194fabb8c1b7b3c782c4ab76a03ec3b0f24581eb4c317ae
4
- data.tar.gz: a290d23df802601977622f1afe39f1f2d6cd92a9350b25d203e97c3b6da6db16
3
+ metadata.gz: 29a7c92a2a313dec57af9a3427cd723224f64bf53cf74a488253741e1098c12d
4
+ data.tar.gz: 4958d188fdddec9fb8143a1b864e9c40d989bbb3360745eb0846687cefc97b9d
5
5
  SHA512:
6
- metadata.gz: a8d72fa4ccb3395f0fdd9203885c1e0cb8a00429597a6dd756e80fb70e0d8092c084992bd9595ee21789a4aaf2550e2284b6d3f81b448dc330088b325d822d85
7
- data.tar.gz: 8f93c98574ef7077336232bb1b51a4af1ac4ba2561914a299209a5c2879fe9af3e8998c8df264836f698fea05c8d028f7ae985de3c18e9889f05568ec2740bc1
6
+ metadata.gz: 2a2a7ecd0a4cb0d84f022c3ec1e0844e2c537cfad88f91b0051609006d8c2f4d596f15a1fd259709c97c1ffccbae87e96758a4bcfb70293d88fb44d7888f4071
7
+ data.tar.gz: 28b108cd63c75d7ead6f34fdd88fdaa867a9601a0217899b024176a4e5508ef9dec7395bf7ae77513de6eb4cc765be90e010146bdd114b989b33359e4ac55068
data/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  ## [Unreleased]
2
2
 
3
+
4
+ ## [0.5.1] - 2024-03-25
5
+
6
+ ### Fixed
7
+
8
+ - Fixed compilation on non-Linux environments.
9
+
10
+
11
+ ## [0.5.0] - 2024-03-25
12
+
13
+ ### Added
14
+
15
+ - `pf2 serve` subcommand
16
+ - `pf2 serve -- ruby target.rb`
17
+ - Profile programs without any change
18
+ - New option: `threads: :all`
19
+ - When specified, Pf2 will track all active threads.
20
+ - `threads: nil` / omitting the `threads` option has the same effect.
21
+ - Introduce `Pf2::Session` (https://github.com/osyoyu/pf2/pull/16)
22
+ - `Session` will be responsible for managing Profiles and Schedulers
23
+
24
+ ### Removed
25
+
26
+ - `Pf2::SignalScheduler` and `Pf2::TimerThreadScheduler` are now hidden from Ruby.
27
+ - `track_all_threads` option is removed in favor of `threads: :all`.
28
+
29
+
30
+ ## [0.4.0] - 2024-03-22
31
+
3
32
  ### Added
4
33
 
5
34
  - New option: `track_all_threads`
data/Cargo.lock CHANGED
@@ -456,9 +456,9 @@ checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
456
456
 
457
457
  [[package]]
458
458
  name = "shlex"
459
- version = "1.2.0"
459
+ version = "1.3.0"
460
460
  source = "registry+https://github.com/rust-lang/crates.io-index"
461
- checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380"
461
+ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
462
462
 
463
463
  [[package]]
464
464
  name = "syn"
data/README.md CHANGED
@@ -13,6 +13,19 @@ Notable Capabilites
13
13
  Usage
14
14
  --------
15
15
 
16
+ ### Quickstart
17
+
18
+ Run your Ruby program through `pf2 serve`.
19
+ Wait a while until Pf2 collects profiles (or until the target program exits), then open the displayed link for visualization.
20
+
21
+ ```
22
+ $ pf2 serve -- ruby target.rb
23
+ [Pf2] Listening on localhost:51502.
24
+ [Pf2] Open https://profiler.firefox.com/from-url/http%3A%2F%2Flocalhost%3A51502%2Fprofile for visualization.
25
+
26
+ I'm the target program!
27
+ ```
28
+
16
29
  ### Profiling
17
30
 
18
31
  Pf2 will collect samples every 10 ms of wall time by default.
@@ -55,11 +68,10 @@ Pf2 accepts the following configuration keys:
55
68
  ```rb
56
69
  Pf2.start(
57
70
  interval_ms: 49, # Integer: The sampling interval in milliseconds (default: 49)
58
- threads: [], # Array<Thread>: A list of Ruby Threads to be tracked (default: `Thread.list`)
59
71
  time_mode: :cpu, # `:cpu` or `:wall`: The sampling timer's mode
60
72
  # (default: `:cpu` for SignalScheduler, `:wall` for TimerThreadScheduler)
61
- track_all_threads: true # Boolean: Whether to track all Threads regardless of `threads` option
62
- # (default: false)
73
+ threads: [th1, th2], # `Array<Thread>` | `:all`: A list of Ruby Threads to be tracked.
74
+ # When `:all` or unspecified, Pf2 will track all active Threads.
63
75
  )
64
76
  ```
65
77
 
data/ext/pf2/src/lib.rs CHANGED
@@ -9,8 +9,12 @@ mod profile;
9
9
  mod profile_serializer;
10
10
  mod ringbuffer;
11
11
  mod sample;
12
+ mod scheduler;
13
+ mod session;
12
14
  #[cfg(target_os = "linux")]
13
15
  mod signal_scheduler;
16
+ #[cfg(not(target_os = "linux"))]
17
+ mod signal_scheduler_unsupported_platform;
14
18
  mod timer_thread_scheduler;
15
19
  mod util;
16
20
 
@@ -6,6 +6,7 @@ use rb_sys::*;
6
6
 
7
7
  use crate::backtrace::Backtrace;
8
8
  use crate::profile::Profile;
9
+ use crate::util::RTEST;
9
10
 
10
11
  #[derive(Debug, Deserialize, Serialize)]
11
12
  pub struct ProfileSerializer {
@@ -62,6 +63,10 @@ struct FrameTableEntry {
62
63
  id: FrameTableId,
63
64
  entry_type: FrameTableEntryType,
64
65
  full_label: String,
66
+ file_name: Option<String>,
67
+ function_first_lineno: Option<i32>,
68
+ callsite_lineno: Option<i32>,
69
+ address: Option<usize>,
65
70
  }
66
71
 
67
72
  #[derive(Debug, Deserialize, Serialize)]
@@ -77,6 +82,11 @@ struct ProfileSample {
77
82
  stack_tree_id: StackTreeNodeId,
78
83
  }
79
84
 
85
+ struct NativeFunctionFrame {
86
+ pub symbol_name: String,
87
+ pub address: Option<usize>,
88
+ }
89
+
80
90
  impl ProfileSerializer {
81
91
  pub fn serialize(profile: &Profile) -> String {
82
92
  let mut sequence = 1;
@@ -92,49 +102,46 @@ impl ProfileSerializer {
92
102
 
93
103
  // Process C-level stack
94
104
 
95
- // A vec to keep the "programmer's" C stack trace.
96
- // A single PC may be mapped to multiple inlined frames,
97
- // so we keep the expanded stack frame in this Vec.
98
- let mut c_stack: Vec<String> = vec![];
105
+ let mut c_stack: Vec<NativeFunctionFrame> = vec![];
106
+ // Rebuild the original backtrace (including inlined functions) from the PC.
99
107
  for i in 0..sample.c_backtrace_pcs[0] {
100
108
  let pc = sample.c_backtrace_pcs[i + 1];
101
109
  Backtrace::backtrace_syminfo(
102
110
  &profile.backtrace_state,
103
111
  pc,
104
- |_pc: usize, symname: *const c_char, _symval: usize, _symsize: usize| {
112
+ |_pc: usize, symname: *const c_char, symval: usize, _symsize: usize| {
105
113
  if symname.is_null() {
106
- c_stack.push("(no symbol information)".to_owned());
114
+ c_stack.push(NativeFunctionFrame {
115
+ symbol_name: "(no symbol information)".to_owned(),
116
+ address: None,
117
+ });
107
118
  } else {
108
- c_stack.push(CStr::from_ptr(symname).to_str().unwrap().to_owned());
119
+ c_stack.push(NativeFunctionFrame {
120
+ symbol_name: CStr::from_ptr(symname)
121
+ .to_str()
122
+ .unwrap()
123
+ .to_owned(),
124
+ address: Some(symval),
125
+ });
109
126
  }
110
127
  },
111
128
  Some(Backtrace::backtrace_error_callback),
112
129
  );
113
130
  }
114
-
115
- // Strip the C stack trace:
116
- // - Remove Pf2-related frames which are always captured
117
- // - Remove frames below rb_vm_exec
118
- let mut reached_ruby = false;
119
- c_stack.retain(|frame| {
120
- if reached_ruby {
121
- return false;
122
- }
123
- if frame.contains("pf2") {
124
- return false;
125
- }
126
- if frame.contains("rb_vm_exec") || frame.contains("vm_call_cfunc_with_frame") {
127
- reached_ruby = true;
128
- return false;
131
+ for frame in c_stack.iter() {
132
+ if frame.symbol_name.contains("pf2") {
133
+ // Skip Pf2-related frames
134
+ continue;
129
135
  }
130
- true
131
- });
132
136
 
133
- for frame in c_stack.iter() {
134
137
  merged_stack.push(FrameTableEntry {
135
- id: calculate_id_for_c_frame(frame),
138
+ id: calculate_id_for_c_frame(&frame.symbol_name),
136
139
  entry_type: FrameTableEntryType::Native,
137
- full_label: frame.to_string(),
140
+ full_label: frame.symbol_name.clone(),
141
+ file_name: None,
142
+ function_first_lineno: None,
143
+ callsite_lineno: None,
144
+ address: frame.address,
138
145
  });
139
146
  }
140
147
 
@@ -143,15 +150,52 @@ impl ProfileSerializer {
143
150
  let ruby_stack_depth = sample.line_count;
144
151
  for i in 0..ruby_stack_depth {
145
152
  let frame: VALUE = sample.frames[i as usize];
153
+ let lineno: i32 = sample.linenos[i as usize];
154
+ let address: Option<usize> = {
155
+ let cme = frame
156
+ as *mut crate::ruby_internal_apis::rb_callable_method_entry_struct;
157
+ let cme = &*cme;
158
+
159
+ if (*(cme.def)).type_ == 1 {
160
+ // The cme is a Cfunc
161
+ Some((*(cme.def)).cfunc.func as usize)
162
+ } else {
163
+ // The cme is an ISeq (Ruby code) or some other type
164
+ None
165
+ }
166
+ };
167
+ let mut frame_full_label: VALUE = rb_profile_frame_full_label(frame);
168
+ let frame_full_label: String = if RTEST(frame_full_label) {
169
+ CStr::from_ptr(rb_string_value_cstr(&mut frame_full_label))
170
+ .to_str()
171
+ .unwrap()
172
+ .to_owned()
173
+ } else {
174
+ "(unknown)".to_owned()
175
+ };
176
+ let mut frame_path: VALUE = rb_profile_frame_path(frame);
177
+ let frame_path: String = if RTEST(frame_path) {
178
+ CStr::from_ptr(rb_string_value_cstr(&mut frame_path))
179
+ .to_str()
180
+ .unwrap()
181
+ .to_owned()
182
+ } else {
183
+ "(unknown)".to_owned()
184
+ };
185
+ let frame_first_lineno: VALUE = rb_profile_frame_first_lineno(frame);
186
+ let frame_first_lineno: Option<i32> = if RTEST(frame_first_lineno) {
187
+ Some(rb_num2int(frame_first_lineno).try_into().unwrap())
188
+ } else {
189
+ None
190
+ };
146
191
  merged_stack.push(FrameTableEntry {
147
192
  id: frame,
148
193
  entry_type: FrameTableEntryType::Ruby,
149
- full_label: CStr::from_ptr(rb_string_value_cstr(
150
- &mut rb_profile_frame_full_label(frame),
151
- ))
152
- .to_str()
153
- .unwrap()
154
- .to_owned(),
194
+ full_label: frame_full_label,
195
+ file_name: Some(frame_path),
196
+ function_first_lineno: frame_first_lineno,
197
+ callsite_lineno: Some(lineno),
198
+ address,
155
199
  });
156
200
  }
157
201
 
@@ -2,9 +2,7 @@
2
2
 
3
3
  use rb_sys::*;
4
4
 
5
- #[cfg(target_os = "linux")]
6
- use crate::signal_scheduler::SignalScheduler;
7
- use crate::timer_thread_scheduler::TimerThreadScheduler;
5
+ use crate::session::ruby_object::SessionRubyObject;
8
6
  use crate::util::*;
9
7
 
10
8
  #[allow(non_snake_case)]
@@ -21,53 +19,24 @@ extern "C" fn Init_pf2() {
21
19
  unsafe {
22
20
  let rb_mPf2: VALUE = rb_define_module(cstr!("Pf2"));
23
21
 
24
- #[cfg(target_os = "linux")]
25
- {
26
- let rb_mPf2_SignalScheduler =
27
- rb_define_class_under(rb_mPf2, cstr!("SignalScheduler"), rb_cObject);
28
- rb_define_alloc_func(rb_mPf2_SignalScheduler, Some(SignalScheduler::rb_alloc));
29
- rb_define_method(
30
- rb_mPf2_SignalScheduler,
31
- cstr!("initialize"),
32
- Some(to_ruby_cfunc_with_args(SignalScheduler::rb_initialize)),
33
- -1,
34
- );
35
- rb_define_method(
36
- rb_mPf2_SignalScheduler,
37
- cstr!("start"),
38
- Some(to_ruby_cfunc_with_no_args(SignalScheduler::rb_start)),
39
- 0,
40
- );
41
- rb_define_method(
42
- rb_mPf2_SignalScheduler,
43
- cstr!("stop"),
44
- Some(to_ruby_cfunc_with_no_args(SignalScheduler::rb_stop)),
45
- 0,
46
- );
47
- }
48
-
49
- let rb_mPf2_TimerThreadScheduler =
50
- rb_define_class_under(rb_mPf2, cstr!("TimerThreadScheduler"), rb_cObject);
51
- rb_define_alloc_func(
52
- rb_mPf2_TimerThreadScheduler,
53
- Some(TimerThreadScheduler::rb_alloc),
54
- );
22
+ let rb_mPf2_Session = rb_define_class_under(rb_mPf2, cstr!("Session"), rb_cObject);
23
+ rb_define_alloc_func(rb_mPf2_Session, Some(SessionRubyObject::rb_alloc));
55
24
  rb_define_method(
56
- rb_mPf2_TimerThreadScheduler,
25
+ rb_mPf2_Session,
57
26
  cstr!("initialize"),
58
- Some(to_ruby_cfunc_with_args(TimerThreadScheduler::rb_initialize)),
27
+ Some(to_ruby_cfunc_with_args(SessionRubyObject::rb_initialize)),
59
28
  -1,
60
29
  );
61
30
  rb_define_method(
62
- rb_mPf2_TimerThreadScheduler,
31
+ rb_mPf2_Session,
63
32
  cstr!("start"),
64
- Some(to_ruby_cfunc_with_no_args(TimerThreadScheduler::rb_start)),
33
+ Some(to_ruby_cfunc_with_no_args(SessionRubyObject::rb_start)),
65
34
  0,
66
35
  );
67
36
  rb_define_method(
68
- rb_mPf2_TimerThreadScheduler,
37
+ rb_mPf2_Session,
69
38
  cstr!("stop"),
70
- Some(to_ruby_cfunc_with_no_args(TimerThreadScheduler::rb_stop)),
39
+ Some(to_ruby_cfunc_with_no_args(SessionRubyObject::rb_stop)),
71
40
  0,
72
41
  );
73
42
  }
@@ -1,12 +1,42 @@
1
1
  #![allow(non_snake_case)]
2
2
  #![allow(non_camel_case_types)]
3
3
 
4
- use libc::{clockid_t, pthread_getcpuclockid, pthread_t};
4
+ use libc::{clockid_t, pthread_t};
5
5
  use rb_sys::{rb_check_typeddata, rb_data_type_struct, RTypedData, VALUE};
6
- use std::ffi::{c_char, c_int};
6
+ use std::ffi::{c_char, c_int, c_void};
7
7
  use std::mem::MaybeUninit;
8
8
 
9
+ #[cfg(target_os = "linux")]
10
+ use libc::pthread_getcpuclockid;
11
+
12
+ #[cfg(not(target_os = "linux"))]
13
+ pub unsafe fn pthread_getcpuclockid(thread: pthread_t, clk_id: *mut clockid_t) -> c_int {
14
+ unimplemented!()
15
+ }
16
+
9
17
  // Types and structs from Ruby 3.4.0.
18
+ #[repr(C)]
19
+ pub struct rb_callable_method_entry_struct {
20
+ /* same fields with rb_method_entry_t */
21
+ pub flags: VALUE,
22
+ _padding_defined_class: VALUE,
23
+ pub def: *mut rb_method_definition_struct,
24
+ // ...
25
+ }
26
+
27
+ #[repr(C)]
28
+ pub struct rb_method_definition_struct {
29
+ pub type_: c_int,
30
+ _padding: [c_char; 4],
31
+ pub cfunc: rb_method_cfunc_struct,
32
+ // ...
33
+ }
34
+
35
+ #[repr(C)]
36
+ pub struct rb_method_cfunc_struct {
37
+ pub func: *mut c_void,
38
+ // ...
39
+ }
10
40
 
11
41
  type rb_nativethread_id_t = libc::pthread_t;
12
42
 
@@ -0,0 +1,10 @@
1
+ use rb_sys::{size_t, VALUE};
2
+
3
+ pub trait Scheduler {
4
+ fn start(&self) -> VALUE;
5
+ fn stop(&self) -> VALUE;
6
+ fn on_new_thread(&self, thread: VALUE);
7
+ fn dmark(&self);
8
+ fn dfree(&self);
9
+ fn dsize(&self) -> size_t;
10
+ }
@@ -0,0 +1,106 @@
1
+ use std::collections::HashSet;
2
+ use std::str::FromStr;
3
+ use std::time::Duration;
4
+
5
+ use rb_sys::*;
6
+
7
+ use crate::util::cstr;
8
+
9
+ pub const DEFAULT_SCHEDULER: Scheduler = Scheduler::Signal;
10
+ pub const DEFAULT_INTERVAL: Duration = Duration::from_millis(49);
11
+ pub const DEFAULT_TIME_MODE: TimeMode = TimeMode::CpuTime;
12
+
13
+ #[derive(Clone, Debug)]
14
+ pub struct Configuration {
15
+ pub scheduler: Scheduler,
16
+ pub interval: Duration,
17
+ pub time_mode: TimeMode,
18
+ pub target_ruby_threads: Threads,
19
+ }
20
+
21
+ #[derive(Clone, Debug, PartialEq)]
22
+ pub enum Scheduler {
23
+ Signal,
24
+ TimerThread,
25
+ }
26
+
27
+ impl FromStr for Scheduler {
28
+ type Err = ();
29
+
30
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
31
+ match s {
32
+ "signal" => Ok(Self::Signal),
33
+ "timer_thread" => Ok(Self::TimerThread),
34
+ _ => Err(()),
35
+ }
36
+ }
37
+ }
38
+
39
+ #[derive(Clone, Debug, PartialEq)]
40
+ pub enum TimeMode {
41
+ CpuTime,
42
+ WallTime,
43
+ }
44
+
45
+ impl FromStr for TimeMode {
46
+ type Err = ();
47
+
48
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
49
+ match s {
50
+ "cpu" => Ok(Self::CpuTime),
51
+ "wall" => Ok(Self::WallTime),
52
+ _ => Err(()),
53
+ }
54
+ }
55
+ }
56
+
57
+ #[derive(Clone, Debug, PartialEq)]
58
+ pub enum Threads {
59
+ All,
60
+ Targeted(HashSet<VALUE>),
61
+ }
62
+
63
+ impl Configuration {
64
+ pub fn validate(&self) -> Result<(), String> {
65
+ if self.scheduler == Scheduler::TimerThread && self.time_mode == TimeMode::CpuTime {
66
+ return Err("TimerThread scheduler does not support `time_mode: :cpu`.".to_owned());
67
+ }
68
+ if self.scheduler == Scheduler::TimerThread && self.target_ruby_threads == Threads::All {
69
+ return Err(concat!(
70
+ "TimerThread scheduler does not support `threads: :all` at the moment. ",
71
+ "Consider using `threads: Thread.list` for watching all threads at profiler start."
72
+ )
73
+ .to_owned());
74
+ }
75
+
76
+ Ok(())
77
+ }
78
+
79
+ pub fn to_rb_hash(&self) -> VALUE {
80
+ let hash: VALUE = unsafe { rb_hash_new() };
81
+ unsafe {
82
+ rb_hash_aset(
83
+ hash,
84
+ rb_id2sym(rb_intern(cstr!("scheduler"))),
85
+ rb_id2sym(rb_intern(match self.scheduler {
86
+ Scheduler::Signal => cstr!("signal"),
87
+ Scheduler::TimerThread => cstr!("timer_thread"),
88
+ })),
89
+ );
90
+ rb_hash_aset(
91
+ hash,
92
+ rb_id2sym(rb_intern(cstr!("interval_ms"))),
93
+ rb_int2inum(self.interval.as_millis().try_into().unwrap()),
94
+ );
95
+ rb_hash_aset(
96
+ hash,
97
+ rb_id2sym(rb_intern(cstr!("time_mode"))),
98
+ rb_id2sym(rb_intern(match self.time_mode {
99
+ TimeMode::CpuTime => cstr!("cpu"),
100
+ TimeMode::WallTime => cstr!("wall"),
101
+ })),
102
+ );
103
+ }
104
+ hash
105
+ }
106
+ }
@@ -0,0 +1,80 @@
1
+ use std::collections::HashSet;
2
+ use std::ffi::c_void;
3
+ use std::mem::ManuallyDrop;
4
+ use std::ptr::null_mut;
5
+ use std::rc::Rc;
6
+ use std::sync::Mutex;
7
+
8
+ use rb_sys::*;
9
+
10
+ /// A helper to watch new Ruby threads.
11
+ ///
12
+ /// `NewThreadWatcher` operates on the Events Hooks API.
13
+ /// Instead of relying on the `THREAD_EVENT_STARTED` event, it combines the
14
+ /// `THREAD_EVENT_RESUMED` event and an internal _known-threads_ record.
15
+ ///
16
+ /// This is to support operations requiring the underlying pthread. Ruby Threads
17
+ /// are not guaranteed to be fully initialized at the time
18
+ /// `THREAD_EVENT_STARTED` is triggered; i.e. the underlying pthread has not
19
+ /// been created yet and `Thread#native_thread_id` returns `nil`.
20
+ pub struct NewThreadWatcher {
21
+ inner: Rc<Mutex<Inner>>,
22
+ event_hook: *mut rb_internal_thread_event_hook_t,
23
+ }
24
+
25
+ struct Inner {
26
+ known_threads: HashSet<VALUE>,
27
+ on_new_thread: Box<dyn Fn(VALUE)>,
28
+ }
29
+
30
+ impl NewThreadWatcher {
31
+ pub fn watch<F>(callback: F) -> Self
32
+ where
33
+ F: Fn(VALUE) + 'static,
34
+ {
35
+ let mut watcher = Self {
36
+ inner: Rc::new(Mutex::new(Inner {
37
+ known_threads: HashSet::new(),
38
+ on_new_thread: Box::new(callback),
39
+ })),
40
+ event_hook: null_mut(),
41
+ };
42
+
43
+ let inner_ptr = Rc::into_raw(Rc::clone(&watcher.inner));
44
+ unsafe {
45
+ watcher.event_hook = rb_internal_thread_add_event_hook(
46
+ Some(Self::on_thread_resume),
47
+ RUBY_INTERNAL_THREAD_EVENT_RESUMED,
48
+ inner_ptr as *mut c_void,
49
+ );
50
+ };
51
+
52
+ watcher
53
+ }
54
+
55
+ unsafe extern "C" fn on_thread_resume(
56
+ _flag: rb_event_flag_t,
57
+ data: *const rb_internal_thread_event_data,
58
+ custom_data: *mut c_void,
59
+ ) {
60
+ let ruby_thread: VALUE = unsafe { (*data).thread };
61
+
62
+ // A pointer to Box<Inner> is passed as custom_data
63
+ let inner = unsafe { ManuallyDrop::new(Box::from_raw(custom_data as *mut Mutex<Inner>)) };
64
+ let mut inner = inner.lock().unwrap();
65
+
66
+ if !inner.known_threads.contains(&ruby_thread) {
67
+ inner.known_threads.insert(ruby_thread);
68
+ (inner.on_new_thread)(ruby_thread);
69
+ }
70
+ }
71
+ }
72
+
73
+ impl Drop for NewThreadWatcher {
74
+ fn drop(&mut self) {
75
+ log::trace!("Cleaning up event hook");
76
+ unsafe {
77
+ rb_internal_thread_remove_event_hook(self.event_hook);
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,90 @@
1
+ use std::ffi::{c_int, c_void};
2
+ use std::mem;
3
+ use std::mem::ManuallyDrop;
4
+ use std::ptr::null_mut;
5
+
6
+ use rb_sys::*;
7
+
8
+ use crate::util::cstr;
9
+
10
+ use super::Session;
11
+
12
+ pub struct SessionRubyObject {
13
+ session: Option<Session>,
14
+ }
15
+
16
+ impl SessionRubyObject {
17
+ pub unsafe extern "C" fn rb_initialize(
18
+ argc: c_int,
19
+ argv: *const VALUE,
20
+ rbself: VALUE,
21
+ ) -> VALUE {
22
+ let mut obj = unsafe { Self::get_struct_from(rbself) };
23
+ obj.session = Some(Session::new_from_rb_initialize(argc, argv, rbself));
24
+ Qnil.into()
25
+ }
26
+
27
+ pub unsafe extern "C" fn rb_start(rbself: VALUE) -> VALUE {
28
+ let mut obj = Self::get_struct_from(rbself);
29
+ match &mut obj.session {
30
+ Some(session) => session.start(),
31
+ None => panic!("Session is not initialized"),
32
+ }
33
+ }
34
+
35
+ pub unsafe extern "C" fn rb_stop(rbself: VALUE) -> VALUE {
36
+ let mut obj = Self::get_struct_from(rbself);
37
+ match &mut obj.session {
38
+ Some(session) => session.stop(),
39
+ None => panic!("Session is not initialized"),
40
+ }
41
+ }
42
+
43
+ // Extract the SessionRubyObject struct from a Ruby object
44
+ unsafe fn get_struct_from(obj: VALUE) -> ManuallyDrop<Box<Self>> {
45
+ unsafe {
46
+ let ptr = rb_check_typeddata(obj, &RBDATA);
47
+ ManuallyDrop::new(Box::from_raw(ptr as *mut SessionRubyObject))
48
+ }
49
+ }
50
+
51
+ #[allow(non_snake_case)]
52
+ pub unsafe extern "C" fn rb_alloc(_rbself: VALUE) -> VALUE {
53
+ let obj = Box::new(SessionRubyObject { session: None });
54
+
55
+ let rb_mPf2: VALUE = rb_define_module(cstr!("Pf2"));
56
+ let rb_cSession = rb_define_class_under(rb_mPf2, cstr!("Session"), rb_cObject);
57
+ // Wrap the struct into a Ruby object
58
+ rb_data_typed_object_wrap(rb_cSession, Box::into_raw(obj) as *mut c_void, &RBDATA)
59
+ }
60
+
61
+ unsafe extern "C" fn dmark(ptr: *mut c_void) {
62
+ let obj = ManuallyDrop::new(Box::from_raw(ptr as *mut SessionRubyObject));
63
+ if let Some(session) = &obj.session {
64
+ session.dmark()
65
+ }
66
+ }
67
+
68
+ unsafe extern "C" fn dfree(ptr: *mut c_void) {
69
+ drop(Box::from_raw(ptr as *mut SessionRubyObject));
70
+ }
71
+
72
+ unsafe extern "C" fn dsize(_: *const c_void) -> size_t {
73
+ // FIXME: Report something better
74
+ mem::size_of::<SessionRubyObject>() as size_t
75
+ }
76
+ }
77
+
78
+ static mut RBDATA: rb_data_type_t = rb_data_type_t {
79
+ wrap_struct_name: cstr!("SessionRubyObject"),
80
+ function: rb_data_type_struct__bindgen_ty_1 {
81
+ dmark: Some(SessionRubyObject::dmark),
82
+ dfree: Some(SessionRubyObject::dfree),
83
+ dsize: Some(SessionRubyObject::dsize),
84
+ dcompact: None,
85
+ reserved: [null_mut(); 1],
86
+ },
87
+ parent: null_mut(),
88
+ data: null_mut(),
89
+ flags: 0,
90
+ };