ShadowLink
ShadowLink is a Ruby gem that enables the exchange of objects which typically cannot be shared between Ractors by emulating them via proxies and converting them into a shareable data format.
Installation
Install the gem and add to the application's Gemfile by executing:
bundle add shadowlink
If bundler is not being used to manage dependencies, install the gem by executing:
gem install shadowlink
Usage
Objects such as ActiveRecord typically cannot be used within a Ractor.
Ractor.new(Monster.all) do |monsters|
monsters.first.id # An error occurs.
end
Passing an object that cannot be shared to ShadowLink converts it into a shareable proxy object.
ShadowLink.lurk(Monster.all) do |shadow_monsters|
Ractor.new(shadow_monsters) do |monsters|
monsters.first.id
end.value
end
You can pass multiple objects.
ShadowLink.lurk(Fairy.new, [Octorok.new, Keese.new]) do |shadow_fairy, shadow_monsters|
Ractor.new(shadow_fairy, shadow_monsters) do |fairy, monsters|
# anything
end.value
end
The last argument of the ShadowLink.lurk block is a Shadow object.
By using the Shadow object's #sink method, you can shadow an object even after ShadowLink.lurk has executed.
ShadowLink.lurk(Fairy.new) do |shadow_fairy, shadow|
shadow_octorok = shadow.sink(Octorok.new)
Ractor.new(shadow_fairy, shadow_octorok, shadow.sink(Keese.all)) do |fairy, octorok, keese|
# anything
end.value
end
Exiting ShadowLink.lurk before the processing inside the Ractor completes causes the port to close, resulting in an error.
Be sure to wait for the operation to complete using Ractor#join or Ractor#value.
ShadowLink.lurk(Fairy.new) do |shadow_fairy|
Ractor.new(shadow_fairy) do |fairy|
sleep 1
fairy.id # raise 'Ractor::Port#send': The port was already closed (Ractor::ClosedError)
end # non wait
end
If the result of the ShadowLink.lurk block is a shadowed object, the result of ShadowLink.lurk is converted to the original object and returned.
ShadowLink.lurk(Fairy.new) do |shadow_fairy|
Ractor.new(shadow_fairy) do |fairy|
fairy # ShadowLink<Fairy> Object
end.value # ShadowLink<Fairy> Object
end # Fairy Object
You can also retrieve the original object by using #seek.
ShadowLink.lurk(Fairy.new) do |shadow_fairy, shadow|
result_fairy = Ractor.new(shadow_fairy) do |fairy|
fairy # ShadowLink<Fairy> Object
end.value # ShadowLink<Fairy> Object
shadow.seek(result_fairy) # Fairy Object
end
When using ShadowLink, an error occurring at the source becomes a Ractor::RuntimeError within ShadowLink.lurk, but upon exiting ShadowLink.lurk, it is converted back into the original error and raised as an exception.
begin
ShadowLink.lurk(-> { raise 'Gameover' }) do |shadow_process|
Ractor.new(shadow_process) do |process|
process.call
end.value
rescue
raise # raise Ractor::RuntimeError
end
rescue
raise # raise Gameover (RuntimeError)
end
Typically, the main process runs on a single thread, but you can increase the number of processing threads by specifying the thread count via a keyword argument.
If you run multiple Ractors within ShadowLink.lurk and share tasks that involve significant I/O waiting, increasing the number of threads should improve processing efficiency.
ShadowLink.lurk(Fairy.new, thread: 5) do |shadow_fairy|
5.times.map do
Ractor.new(shadow_fairy) do |fairy|
# anything
end
end.each(:join)
end
ShadowLink does not convert shareable objects; it passes the data through as-is.
Objects that cannot be shared directly are shared via proxy objects.
As an exception, String objects are by default converted into shareable objects via Ractor.make_shareable.
If there are objects other than Strings that you wish to make shareable using Ractor.make_shareable, you can specify them using keyword arguments.
ShadowLink.lurk(any, make_shareables: [String, Array, Hash]) do |shadow_any|
Ractor.new(shadow_any) do |obj|
obj.to_s # Frozen original String object
obj.to_a # Frozen original Array object
obj.to_h # Frozen original Hash object
end.value
end
If you do not want a String to be converted into a shareable object, you can prevent the conversion by specifying an array that does not contain the String. I wouldn't recommend it, as it causes various things to stop working.
string = 'zelda'.dup
ShadowLink.lurk(string, make_shareables: []) do |shadow_string|
Ractor.new(shadow_string) do |str|
str.gsub!('zelda', 'sheik')
# However, you cannot do `puts str`
puts str == 'sheik' # false
end.join
end
puts string == 'sheik' # true
Performance
Performance verification is conducted in the following environment.
% system_profiler SPHardwareDataType
Hardware:
Hardware Overview:
Model Name: MacBook Pro
Model Identifier: Mac16,8
Model Number: Z1FE000FHJ/A
Chip: Apple M4 Pro
Total Number of Cores: 12 (8 Performance and 4 Efficiency)
Memory: 48 GB
System Firmware Version: 18000.120.36
OS Loader Version: 18000.120.36
% ruby -v
ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +PRISM [arm64-darwin25]
For method calls without arguments, execution is possible with the following performance difference.
require 'benchmark'
Benchmark.bm do |x|
array = (1..10000).to_a
x.report('non ractor') do
array.each { array.sum }
end
x.report('use frozen object') do
frozen_array = array.dup.freeze
array.each do
Ractor.new(frozen_array) { |arr| arr.sum }.join
end
end
x.report('use shadowlink') do
ShadowLink.lurk(array) do |shadow_array|
array.each do
Ractor.new(shadow_array) { |arr| arr.sum }.join
end
end
end
end
user system total real
non ractor 0.084500 0.000373 0.084873 ( 0.085017)
use frozen object 0.131565 0.071487 0.203052 ( 0.203015)
use shadowlink 0.224832 0.189211 0.414043 ( 0.379295)
Methods that accept only shareable objects as arguments can be called with the following performance differences.
require 'benchmark'
Benchmark.bm do |x|
array = (1..10000).to_a
x.report('non ractor') do
array.each { array.sum(0) }
end
x.report('use frozen object') do
frozen_array = array.dup.freeze
array.each do
Ractor.new(frozen_array) { |arr| arr.sum(0) }.join
end
end
x.report('use shadowlink') do
ShadowLink.lurk(array) do |shadow_array|
array.each do
Ractor.new(shadow_array) { |arr| arr.sum(0) }.join
end
end
end
end
user system total real
non ractor 0.082363 0.000367 0.082730 ( 0.082839)
use frozen object 0.128988 0.073280 0.202268 ( 0.202182)
use shadowlink 0.229528 0.192023 0.421551 ( 0.385374)
Methods containing objects that cannot be shared can be called with the following performance differences.
require 'benchmark'
Benchmark.bm do |x|
array = (1..100).to_a
x.report('non ractor') do
array.each { array.sum { |i| i * 2 } }
end
x.report('use frozen object') do
frozen_array = array.dup.freeze
array.each do
Ractor.new(frozen_array) { |arr| arr.sum { |i| i * 2 } }.join
end
end
x.report('use shadowlink') do
ShadowLink.lurk(array) do |shadow_array|
array.each do
Ractor.new(shadow_array) { |arr| arr.sum { |i| i * 2 } }.join
end
end
end
end
user system total real
non ractor 0.000415 0.000014 0.000429 ( 0.000426)
use frozen object 0.002826 0.002211 0.005037 ( 0.004948)
use shadowlink 0.079253 0.076243 0.155496 ( 0.149635)
In practice, parallelization can reduce processing time. For example, if you need to read characters 1,000 times with each read taking 0.001 seconds processing them in 10 parallel threads can reduce the execution time to nearly one-tenth of the original.
require 'benchmark'
Benchmark.bm do |x|
process = -> { sleep 0.001; 'result' }
x.report('non ractor') do
1000.times.each { process.call }
end
x.report('use thread') do
10.times.map do
Thread.new do
100.times.each { process.call }
end
end.each(&:join)
end
x.report('use shadowlink') do
ShadowLink.lurk(process, threads: 10) do |shadow_process|
10.times.map do
Ractor.new(shadow_process) do |p|
100.times.map { p.call }
end
end.each(&:join)
end
end
end
user system total real
non ractor 0.003898 0.010412 0.014310 ( 1.269621)
use thread 0.003713 0.013605 0.017318 ( 0.127920)
use shadowlink 0.026961 0.033084 0.060045 ( 0.140258)
In this example, threads are faster; however, the difference lies in the ability to process conditional branching and basic arithmetic operations in parallel. In cases where calculations are performed simultaneously with data input and output, extremely high-speed data processing becomes possible.
Development
After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/ucks/shadowlink-rb.