pf2 0.4.0 → 0.5.1

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: 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
+ };