railst 0.2.0 → 0.2.2

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: 664b3b8d4db44f5d103ad5e55f0a3c21cf1656caf054196dbde9dcf1f2ec0808
4
- data.tar.gz: 8c4829858987a562b87f035a42dd93451eeba1ea312dc62fc5dc78fceb32af80
3
+ metadata.gz: 39b34a69f1512f12b3d0cafab0626efaf8d26e420a744c6c7ee8cd81c8c4b416
4
+ data.tar.gz: eeffce1eb4528445d31f36c5e5c61c4a411fab859a8663141ac3ebf7468cb814
5
5
  SHA512:
6
- metadata.gz: 6cf43b50d8f5d497975d5e9f20dea6aeaee2dd6f871c807f2960a81c49bcabfcf0c5b3d8e7d757bb13e836deebe586e00b530bcf5dd04cb6cdcac0e2aeca5f61
7
- data.tar.gz: b53df00d4dd3bdab8889868edcb59cd7b6536ce6f718fb2798b35877ee47738a8752ead873afcf6d2991eeecefec58c711e5520db27a709fcc6ef911868895ae
6
+ metadata.gz: 991cebc2dbc0908279c391ad311ac7f1e3231078b0d4f1bcb56411f891b10c387b48c8f28ec3d65d5e810f0832727e775d6d3ecb6a3898bfc5d7d7722d2f53ef
7
+ data.tar.gz: 22e2646d19cb39ac9a3779b9c359c32f6cebab0185efff2773c6e6cde76cd0dcafa89fec90de6ae177b549162e7793760f32f2a870b1b276a667722846e99ad4
data/README.md CHANGED
@@ -11,7 +11,7 @@ gem install railst
11
11
 
12
12
  ### Via One-Line Shell Script
13
13
  ```bash
14
- curl -fsSL https://raw.githubusercontent.com/HarmlessValve/Rust-on-Rails/main/install.sh | bash
14
+ curl -fsSL https://raw.githubusercontent.com/Valirc-cpu/Rust-on-Rails/main/install.sh | bash
15
15
  ```
16
16
 
17
17
  ## Quick Start
data/lib/railst/cli.rb CHANGED
@@ -440,13 +440,17 @@ module Railst
440
440
 
441
441
  Templates.inject_into(target_dir, project_root)
442
442
 
443
- # Append rb_sys into Gemfile if not present
443
+ # Append rb_sys and railst into Gemfile if not present
444
444
  gemfile = target_dir.join("Gemfile")
445
445
  if gemfile.exist?
446
446
  gem_content = gemfile.read
447
- unless gem_content.include?("rb_sys")
447
+ missing_gems = []
448
+ missing_gems << ' gem "railst", "~> 0.2"' unless gem_content.include?("railst")
449
+ missing_gems << ' gem "rb_sys", "~> 0.9"' unless gem_content.include?("rb_sys")
450
+
451
+ unless missing_gems.empty?
448
452
  File.open(gemfile, "a") do |f|
449
- f.puts "\n# Rust C-extension build tooling\ngroup :development, :test do\n gem \"rb_sys\", \"~> 0.9\"\nend\n"
453
+ f.puts "\n# Rust on Rails development & build tooling\ngroup :development, :test do\n#{missing_gems.join("\n")}\nend\n"
450
454
  end
451
455
  end
452
456
  end
@@ -463,7 +467,13 @@ module Railst
463
467
 
464
468
  Dir.chdir(target_dir) do
465
469
  Kernel.system(Environment.toolchains_env, "bundle", "install")
466
- Kernel.system(Environment.toolchains_env, "bin/rake", "rust:build")
470
+ success = Kernel.system(Environment.toolchains_env, "bin/rake", "rust:build")
471
+ unless success
472
+ puts ""
473
+ puts " #{Terminal.badge("ERROR", bg: :red, fg: :white)} Failed to compile native Rust backend."
474
+ puts ""
475
+ exit 1
476
+ end
467
477
  end
468
478
 
469
479
  puts ""
@@ -92,7 +92,7 @@ module Railst
92
92
  crate-type = ["cdylib"]
93
93
 
94
94
  [dependencies]
95
- magnus = "0.9"
95
+ magnus = { version = "0.9", features = ["rb-sys"] }
96
96
  rb-sys = "0.9"
97
97
  rayon = "1.10"
98
98
  serde = { version = "1.0", features = ["derive"] }
@@ -113,30 +113,53 @@ module Railst
113
113
 
114
114
  def self.lib_rs_content
115
115
  <<~RUST
116
- use magnus::{Error, Ruby};
117
-
118
- pub mod dynamic_hash;
119
116
  pub mod data_processor;
117
+ pub mod dynamic_hash;
120
118
  pub mod gvl;
121
119
 
120
+ use magnus::{function, prelude::*, Error, RHash, Ruby};
121
+
122
+ fn hello(name: String) -> String {
123
+ format!("Hello {} from Rust (Magnus)!", name)
124
+ }
125
+
126
+ fn calculate(ruby: &Ruby, a: f64, b: f64, op: String) -> Result<f64, Error> {
127
+ match op.as_str() {
128
+ "add" | "+" => Ok(a + b),
129
+ "sub" | "-" => Ok(a - b),
130
+ "mul" | "*" => Ok(a * b),
131
+ "div" | "/" => {
132
+ if b == 0.0 {
133
+ Err(Error::new(ruby.exception_zero_div_error(), "division by zero in Rust backend"))
134
+ } else {
135
+ Ok(a / b)
136
+ }
137
+ }
138
+ _ => Err(Error::new(ruby.exception_arg_error(), format!("unknown operator: {}", op))),
139
+ }
140
+ }
141
+
142
+ fn process_record(ruby: &Ruby, attributes: RHash) -> Result<RHash, Error> {
143
+ let result = ruby.hash_new();
144
+ attributes.foreach(|k: magnus::Value, v: magnus::Value| {
145
+ result.aset(k, v)?;
146
+ Ok(magnus::r_hash::ForEach::Continue)
147
+ })?;
148
+ result.aset("rust_processed", true)?;
149
+ result.aset("rust_engine", "magnus-0.9")?;
150
+ Ok(result)
151
+ }
152
+
122
153
  #[magnus::init]
123
154
  fn init(ruby: &Ruby) -> Result<(), Error> {
124
155
  let module = ruby.define_module("RustBackend")?;
156
+ module.define_singleton_method("hello", function!(hello, 1))?;
157
+ module.define_singleton_method("calculate", function!(calculate, 3))?;
158
+ module.define_singleton_method("process_record", function!(process_record, 1))?;
125
159
 
126
- module.define_singleton_method(
127
- "process_batch",
128
- magnus::function!(data_processor::process_batch, 2),
129
- )?;
130
-
131
- module.define_singleton_method(
132
- "aggregate_metrics",
133
- magnus::function!(data_processor::aggregate_metrics, 2),
134
- )?;
135
-
136
- module.define_singleton_method(
137
- "transform_record",
138
- magnus::function!(data_processor::transform_record, 2),
139
- )?;
160
+ // High performance data processing with dynamic Hashes & Rayon (GVL released)
161
+ module.define_singleton_method("process_batch", function!(data_processor::process_batch, 2))?;
162
+ module.define_singleton_method("aggregate_metrics", function!(data_processor::aggregate_metrics, 2))?;
140
163
 
141
164
  Ok(())
142
165
  }
@@ -145,123 +168,130 @@ module Railst
145
168
 
146
169
  def self.gvl_rs_content
147
170
  <<~RUST
148
- use magnus::rb_sys::rb_thread_call_without_gvl;
149
- use std::os::raw::c_void;
150
-
151
- extern "C" fn nogvl_trampoline<F, R>(ptr: *mut c_void) -> *mut c_void
152
- where
153
- F: FnOnce() -> R,
154
- {
155
- let f = unsafe { Box::from_raw(ptr as *mut F) };
156
- let result = f();
157
- Box::into_raw(Box::new(result)) as *mut c_void
158
- }
171
+ use std::ffi::c_void;
159
172
 
160
173
  pub fn call_without_gvl<F, R>(f: F) -> R
161
174
  where
162
175
  F: FnOnce() -> R + Send,
163
176
  R: Send,
164
177
  {
165
- let boxed_f = Box::new(f);
166
- let ptr = Box::into_raw(boxed_f) as *mut c_void;
178
+ struct State<F, R> {
179
+ f: Option<F>,
180
+ result: Option<R>,
181
+ }
167
182
 
168
- let res_ptr = unsafe {
169
- rb_thread_call_without_gvl(
170
- Some(nogvl_trampoline::<F, R>),
171
- ptr,
183
+ unsafe extern "C" fn runner<F, R>(data: *mut c_void) -> *mut c_void
184
+ where
185
+ F: FnOnce() -> R + Send,
186
+ R: Send,
187
+ {
188
+ let state = &mut *(data as *mut State<F, R>);
189
+ if let Some(f) = state.f.take() {
190
+ state.result = Some(f());
191
+ }
192
+ std::ptr::null_mut()
193
+ }
194
+
195
+ let mut state = State {
196
+ f: Some(f),
197
+ result: None,
198
+ };
199
+
200
+ unsafe {
201
+ rb_sys::rb_thread_call_without_gvl(
202
+ Some(runner::<F, R>),
203
+ &mut state as *mut _ as *mut c_void,
172
204
  None,
173
205
  std::ptr::null_mut(),
174
- )
175
- };
206
+ );
207
+ }
176
208
 
177
- let boxed_res = unsafe { Box::from_raw(res_ptr as *mut R) };
178
- *boxed_res
209
+ state.result.expect("GVL callback finished")
179
210
  }
180
211
  RUST
181
212
  end
182
213
 
183
214
  def self.dynamic_hash_rs_content
184
215
  <<~RUST
185
- use magnus::{value::ReprValue, Error, RArray, RHash, Ruby, Symbol, Value};
216
+ use magnus::{
217
+ prelude::*, rb_sys::AsRawValue, Error, Float, Integer, RArray, RHash, RString, Ruby, Symbol, Value,
218
+ };
186
219
  use serde_json::{Map, Number, Value as JsonValue};
187
220
 
188
221
  pub fn rval_to_json(val: Value) -> JsonValue {
189
- if val.is_nil() {
190
- return JsonValue::Null;
191
- }
192
- if let Ok(b) = bool::try_convert(val) {
193
- return JsonValue::Bool(b);
194
- }
195
- if let Ok(i) = i64::try_convert(val) {
196
- return JsonValue::Number(Number::from(i));
197
- }
198
- if let Ok(f) = f64::try_convert(val) {
199
- if let Some(n) = Number::from_f64(f) {
200
- return JsonValue::Number(n);
201
- }
202
- }
203
- if let Ok(s) = String::try_convert(val) {
204
- return JsonValue::String(s);
205
- }
206
- if let Ok(sym) = Symbol::try_convert(val) {
207
- if let Ok(s) = sym.name() {
208
- return JsonValue::String(s.to_string());
222
+ let raw = val.as_raw();
223
+ // CRuby special constants: Qnil = 8, Qtrue = 20, Qfalse = 0
224
+ if raw == 8 || val.is_nil() {
225
+ JsonValue::Null
226
+ } else if raw == 20 {
227
+ JsonValue::Bool(true)
228
+ } else if raw == 0 {
229
+ JsonValue::Bool(false)
230
+ } else if let Some(i) = Integer::from_value(val) {
231
+ if let Ok(num) = i.to_i64() {
232
+ JsonValue::Number(num.into())
233
+ } else {
234
+ JsonValue::Number(i.to_u64().unwrap_or(0).into())
209
235
  }
210
- }
211
- if let Ok(hash) = RHash::try_convert(val) {
236
+ } else if let Some(f) = Float::from_value(val) {
237
+ Number::from_f64(f.to_f64())
238
+ .map(JsonValue::Number)
239
+ .unwrap_or(JsonValue::Null)
240
+ } else if let Some(s) = RString::from_value(val) {
241
+ JsonValue::String(s.to_string().unwrap_or_default())
242
+ } else if let Some(sym) = Symbol::from_value(val) {
243
+ JsonValue::String(sym.name().unwrap_or_default().to_string())
244
+ } else if let Some(h) = RHash::from_value(val) {
212
245
  let mut map = Map::new();
213
- let _ = hash.each(|k: Value, v: Value| {
214
- let key_str = if let Ok(s) = String::try_convert(k) {
215
- s
216
- } else if let Ok(sym) = Symbol::try_convert(k) {
217
- sym.name().map(|n| n.to_string()).unwrap_or_else(|_| k.to_string())
246
+ let _ = h.foreach(|k: Value, v: Value| {
247
+ let key = if let Some(s) = RString::from_value(k) {
248
+ s.to_string().unwrap_or_default()
249
+ } else if let Some(sym) = Symbol::from_value(k) {
250
+ sym.name().unwrap_or_default().to_string()
218
251
  } else {
219
252
  k.to_string()
220
253
  };
221
- map.insert(key_str, rval_to_json(v));
222
- Ok(())
254
+ map.insert(key, rval_to_json(v));
255
+ Ok(magnus::r_hash::ForEach::Continue)
223
256
  });
224
- return JsonValue::Object(map);
225
- }
226
- if let Ok(arr) = RArray::try_convert(val) {
227
- let mut vec = Vec::with_capacity(arr.len());
228
- for item in arr.into_iter() {
229
- vec.push(rval_to_json(item));
230
- }
231
- return JsonValue::Array(vec);
257
+ JsonValue::Object(map)
258
+ } else if let Some(arr) = RArray::from_value(val) {
259
+ let vec: Vec<JsonValue> = arr.into_iter().map(rval_to_json).collect();
260
+ JsonValue::Array(vec)
261
+ } else {
262
+ JsonValue::String(val.to_string())
232
263
  }
233
- JsonValue::String(val.to_string())
234
264
  }
235
265
 
236
266
  pub fn json_to_rval(ruby: &Ruby, val: &JsonValue) -> Result<Value, Error> {
237
267
  match val {
238
268
  JsonValue::Null => Ok(ruby.qnil().as_value()),
239
- JsonValue::Bool(b) => Ok(ruby.into_value(*b)),
269
+ JsonValue::Bool(b) => Ok(ruby.into_value(*b).as_value()),
240
270
  JsonValue::Number(n) => {
241
271
  if let Some(i) = n.as_i64() {
242
- Ok(ruby.into_value(i))
272
+ Ok(ruby.into_value(i).as_value())
243
273
  } else if let Some(f) = n.as_f64() {
244
- Ok(ruby.into_value(f))
274
+ Ok(ruby.into_value(f).as_value())
245
275
  } else {
246
- Ok(ruby.into_value(n.to_string()))
276
+ Ok(ruby.qnil().as_value())
247
277
  }
248
278
  }
249
- JsonValue::String(s) => Ok(ruby.into_value(s.as_str())),
279
+ JsonValue::String(s) => Ok(ruby.str_new(s).as_value()),
250
280
  JsonValue::Array(arr) => {
251
- let rarr = ruby.ary_new_capa(arr.len());
281
+ let r_arr = ruby.ary_new_capa(arr.len());
252
282
  for item in arr {
253
- rarr.push(json_to_rval(ruby, item)?)?;
283
+ r_arr.push(json_to_rval(ruby, item)?)?;
254
284
  }
255
- Ok(rarr.as_value())
285
+ Ok(r_arr.as_value())
256
286
  }
257
287
  JsonValue::Object(map) => {
258
- let rhash = ruby.hash_new();
288
+ let r_hash = ruby.hash_new();
259
289
  for (k, v) in map {
260
- let rkey = ruby.into_value(k.as_str());
261
- let rval = json_to_rval(ruby, v)?;
262
- rhash.aset(rkey, rval)?;
290
+ let rk = ruby.str_new(k);
291
+ let rv = json_to_rval(ruby, v)?;
292
+ r_hash.aset(rk, rv)?;
263
293
  }
264
- Ok(rhash.as_value())
294
+ Ok(r_hash.as_value())
265
295
  }
266
296
  }
267
297
  }
@@ -276,23 +306,8 @@ module Railst
276
306
  use rayon::prelude::*;
277
307
  use serde_json::{json, Value as JsonValue};
278
308
 
279
- pub fn transform_record(ruby: &Ruby, record: RHash, config: RHash) -> Result<Value, Error> {
280
- let mut raw_record = rval_to_json(record.as_value());
281
- let raw_config = rval_to_json(config.as_value());
282
-
283
- let field = raw_config.get("compute_field").and_then(|v| v.as_str()).unwrap_or("value").to_string();
284
- let mult = raw_config.get("multiplier").and_then(|v| v.as_f64()).unwrap_or(1.0);
285
-
286
- if let JsonValue::Object(ref mut map) = raw_record {
287
- let current_val = map.get(&field).and_then(|v| v.as_f64()).unwrap_or(0.0);
288
- map.insert(format!("{}_computed", field), json!(current_val * mult));
289
- map.insert("processed_by".to_string(), json!("rust_magnus"));
290
- }
291
-
292
- json_to_rval(ruby, &raw_record)
293
- }
294
-
295
309
  pub fn process_batch(ruby: &Ruby, records: RArray, config: RHash) -> Result<Value, Error> {
310
+ // 1. Convert dynamic Ruby objects to Rust JsonValues while holding GVL
296
311
  let raw_records: Vec<JsonValue> = records.into_iter().map(rval_to_json).collect();
297
312
  let raw_config = rval_to_json(config.as_value());
298
313
 
@@ -312,6 +327,7 @@ module Railst
312
327
  .and_then(|v| v.as_f64())
313
328
  .unwrap_or(0.0);
314
329
 
330
+ // 2. Release GVL and process in parallel across all CPU cores with Rayon
315
331
  let processed_records = call_without_gvl(move || {
316
332
  raw_records
317
333
  .into_par_iter()
@@ -336,6 +352,7 @@ module Railst
336
352
  .collect::<Vec<JsonValue>>()
337
353
  });
338
354
 
355
+ // 3. Serialize back into native Ruby Hash / Array
339
356
  let result_json = JsonValue::Array(processed_records);
340
357
  json_to_rval(ruby, &result_json)
341
358
  }
@@ -344,6 +361,7 @@ module Railst
344
361
  let raw_records: Vec<JsonValue> = records.into_iter().map(rval_to_json).collect();
345
362
  let target_field = metric_field.clone();
346
363
 
364
+ // Release GVL for parallel reduction across all cores
347
365
  let (count, sum, min, max) = call_without_gvl(move || {
348
366
  let values: Vec<f64> = raw_records
349
367
  .into_par_iter()
@@ -383,6 +401,39 @@ module Railst
383
401
 
384
402
  json_to_rval(ruby, &summary)
385
403
  }
404
+
405
+ #[cfg(test)]
406
+ mod tests {
407
+ use super::*;
408
+
409
+ #[test]
410
+ fn test_parallel_reduction_metrics() {
411
+ let records = vec![
412
+ json!({ "reading": 10.0 }),
413
+ json!({ "reading": 20.0 }),
414
+ json!({ "reading": 30.0 }),
415
+ ];
416
+
417
+ let target_field = "reading".to_string();
418
+ let values: Vec<f64> = records
419
+ .into_par_iter()
420
+ .filter_map(|rec| {
421
+ if let JsonValue::Object(map) = rec {
422
+ map.get(&target_field).and_then(|v| v.as_f64())
423
+ } else {
424
+ None
425
+ }
426
+ })
427
+ .collect();
428
+
429
+ assert_eq!(values.len(), 3);
430
+ let sum: f64 = values.par_iter().sum();
431
+ assert_eq!(sum, 60.0);
432
+ let count = values.len();
433
+ let avg = sum / count as f64;
434
+ assert_eq!(avg, 20.0);
435
+ }
436
+ }
386
437
  RUST
387
438
  end
388
439
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Railst
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.2"
5
5
  end
metadata CHANGED
@@ -1,10 +1,10 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: railst
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
- - HarmlessValve
7
+ - Valirc-cpu
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
@@ -12,7 +12,7 @@ dependencies: []
12
12
  description: Standalone developer interface and scaffolding tool for combining Rails
13
13
  8 and pure Rust
14
14
  email:
15
- - Altagemilang@protonmail.com
15
+ - taltagemilang@gmail.com
16
16
  executables:
17
17
  - railst
18
18
  extensions: []
@@ -26,10 +26,13 @@ files:
26
26
  - lib/railst/templates.rb
27
27
  - lib/railst/terminal.rb
28
28
  - lib/railst/version.rb
29
- homepage: https://github.com/HarmlessValve/Rust-on-Rails
29
+ homepage: https://github.com/Valirc-cpu/Rust-on-Rails
30
30
  licenses:
31
31
  - MIT
32
- metadata: {}
32
+ metadata:
33
+ source_code_uri: https://github.com/Valirc-cpu/Rust-on-Rails
34
+ bug_tracker_uri: https://github.com/Valirc-cpu/Rust-on-Rails/issues
35
+ changelog_uri: https://github.com/Valirc-cpu/Rust-on-Rails/blob/main/README.md
33
36
  rdoc_options: []
34
37
  require_paths:
35
38
  - lib