Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Thread local variables #1682

Merged
merged 10 commits into from
Jan 9, 2024
6 changes: 6 additions & 0 deletions include/natalie/thread_object.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ class ThreadObject : public Object {
Value ref(Env *env, Value key);
Value refeq(Env *env, Value key, Value value);

bool has_thread_variable(Env *, Value) const;
Value thread_variable_get(Env *, Value);
Value thread_variable_set(Env *, Value, Value);
Value thread_variables(Env *) const;

void set_sleeping(bool sleeping) { m_sleeping = sleeping; }
bool is_sleeping() const { return m_sleeping; }

Expand Down Expand Up @@ -239,6 +244,7 @@ class ThreadObject : public Object {
std::thread m_thread {};
std::atomic<ExceptionObject *> m_exception { nullptr };
Value m_value { nullptr };
HashObject *m_thread_variables { nullptr };
FiberObject *m_main_fiber { nullptr };
FiberObject *m_current_fiber { nullptr };
#ifdef __SANITIZE_ADDRESS__
Expand Down
4 changes: 4 additions & 0 deletions lib/natalie/compiler/binding_gen.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1344,6 +1344,10 @@ def generate_name
gen.binding('Thread', 'run', 'ThreadObject', 'run', argc: 0, pass_env: true, pass_block: false, return_type: :Object)
gen.binding('Thread', 'status', 'ThreadObject', 'status', argc: 0, pass_env: true, pass_block: false, return_type: :Object)
gen.binding('Thread', 'stop?', 'ThreadObject', 'is_stopped', argc: 0, pass_env: false, pass_block: false, return_type: :bool)
gen.binding('Thread', 'thread_variable?', 'ThreadObject', 'has_thread_variable', argc: 1, pass_env: true, pass_block: false, return_type: :bool)
gen.binding('Thread', 'thread_variable_get', 'ThreadObject', 'thread_variable_get', argc: 1, pass_env: true, pass_block: false, return_type: :Object)
gen.binding('Thread', 'thread_variable_set', 'ThreadObject', 'thread_variable_set', argc: 2, pass_env: true, pass_block: false, return_type: :Object)
gen.binding('Thread', 'thread_variables', 'ThreadObject', 'thread_variables', argc: 0, pass_env: true, pass_block: false, return_type: :Object)
gen.binding('Thread', 'to_s', 'ThreadObject', 'to_s', argc: 0, pass_env: true, pass_block: false, aliases: ['inspect'], return_type: :Object)
gen.binding('Thread', 'value', 'ThreadObject', 'value', argc: 0, pass_env: true, pass_block: false, return_type: :Object)
gen.binding('Thread', 'wakeup', 'ThreadObject', 'wakeup', argc: 0, pass_env: true, pass_block: false, return_type: :Object)
Expand Down
25 changes: 25 additions & 0 deletions spec/core/thread/thread_variable_get_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
require_relative '../../spec_helper'

describe "Thread#thread_variable_get" do
before :each do
@t = Thread.new { }
end

after :each do
@t.join
end

it "returns nil if the variable is not set" do
@t.thread_variable_get(:a).should be_nil
end

it "returns the value previously set by #thread_variable_set" do
@t.thread_variable_set :a, 49
@t.thread_variable_get(:a).should == 49
end

it "returns a value private to self" do
@t.thread_variable_set :thread_variable_get_spec, 82
Thread.current.thread_variable_get(:thread_variable_get_spec).should be_nil
end
end
26 changes: 26 additions & 0 deletions spec/core/thread/thread_variable_set_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
require_relative '../../spec_helper'

describe "Thread#thread_variable_set" do
before :each do
@t = Thread.new { }
end

after :each do
@t.join
end

it "returns the value set" do
(@t.thread_variable_set :a, 2).should == 2
end

it "sets a value that will be returned by #thread_variable_get" do
@t.thread_variable_set :a, 49
@t.thread_variable_get(:a).should == 49
end

it "sets a value private to self" do
@t.thread_variable_set :thread_variable_get_spec, 82
@t.thread_variable_get(:thread_variable_get_spec).should == 82
Thread.current.thread_variable_get(:thread_variable_get_spec).should be_nil
end
end
21 changes: 21 additions & 0 deletions spec/core/thread/thread_variable_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
require_relative '../../spec_helper'

describe "Thread#thread_variable?" do
before :each do
@t = Thread.new { }
end

after :each do
@t.join
end

it "returns false if the thread variables do not contain 'key'" do
@t.thread_variable_set :a, 2
@t.thread_variable?(:b).should be_false
end

it "returns true if the thread variables contain 'key'" do
@t.thread_variable_set :a, 2
@t.thread_variable?(:a).should be_true
end
end
29 changes: 29 additions & 0 deletions spec/core/thread/thread_variables_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
require_relative '../../spec_helper'

describe "Thread#thread_variables" do
before :each do
@t = Thread.new { }
end

after :each do
@t.join
end

it "returns the keys of all the values set" do
@t.thread_variable_set :a, 2
@t.thread_variable_set :b, 4
@t.thread_variable_set :c, 6
@t.thread_variables.sort.should == [:a, :b, :c]
end

it "sets a value private to self" do
@t.thread_variable_set :a, 82
@t.thread_variable_set :b, 82
Thread.current.thread_variables.should_not include(:a, :b)
end

it "only contains user thread variables and is empty initially" do
Thread.current.thread_variables.should == []
@t.thread_variables.should == []
end
end
39 changes: 39 additions & 0 deletions src/thread_object.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,44 @@ Value ThreadObject::refeq(Env *env, Value key, Value value) {
return value;
}

bool ThreadObject::has_thread_variable(Env *env, Value key) const {
if (!key->is_symbol() && !key->is_string() && key->respond_to(env, "to_str"_s))
key = key->to_str(env);
if (key->is_string())
key = key->as_string()->to_sym(env);
return m_thread_variables && m_thread_variables->has_key(env, key);
}

Value ThreadObject::thread_variable_get(Env *env, Value key) {
if (!m_thread_variables)
return NilObject::the();
if (!key->is_symbol() && !key->is_string() && key->respond_to(env, "to_str"_s))
key = key->to_str(env);
if (key->is_string())
key = key->as_string()->to_sym(env);
return m_thread_variables->ref(env, key);
}

Value ThreadObject::thread_variable_set(Env *env, Value key, Value value) {
if (is_frozen())
env->raise("FrozenError", "can't modify frozen thread locals");
if (!key->is_symbol() && !key->is_string() && key->respond_to(env, "to_str"_s))
key = key->to_str(env);
if (key->is_string())
key = key->as_string()->to_sym(env);
if (!key->is_symbol())
env->raise("TypeError", "{} is not a symbol", key->inspect_str(env));
if (!m_thread_variables)
m_thread_variables = new HashObject;
return m_thread_variables->refeq(env, key, value);
}

Value ThreadObject::thread_variables(Env *env) const {
if (!m_thread_variables)
return new ArrayObject;
return m_thread_variables->keys(env);
}

Value ThreadObject::list(Env *env) {
std::lock_guard<std::mutex> lock(g_thread_mutex);
auto ary = new ArrayObject { s_list.size() };
Expand Down Expand Up @@ -480,6 +518,7 @@ void ThreadObject::visit_children(Visitor &visitor) {
visitor.visit(m_exception);
visitor.visit(m_main_fiber);
visitor.visit(m_value);
visitor.visit(m_thread_variables);
for (auto pair : m_mutexes)
visitor.visit(pair.first);
visitor.visit(m_fiber_scheduler);
Expand Down
101 changes: 101 additions & 0 deletions test/natalie/thread_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,105 @@ def create_threads
t1.report_on_exception.should == false
end
end

describe 'Thread#thread_variable?' do
before :each do
@t = Thread.new { }
end

after :each do
@t.join
end

it 'converts String keys to Symbol' do
@t.thread_variable_set(:a, 42)
@t.should.thread_variable?('a')
end

it 'tries to convert the key that is neither String nor Symbol with #to_str' do
key = mock('key')
key.should_receive(:to_str).and_return('a')
@t.thread_variable_set(:a, 42)
@t.should.thread_variable?(key)
end

it 'does not raise a TypeError if the key is not a Symbol' do
@t.should_not.thread_variable?(123)
end

it 'does not try to convert the key with #to_sym' do
key = mock('key')
key.should_not_receive(:to_sym)
@t.should_not.thread_variable?(key)
end
end

describe 'Thread#thread_variable_get' do
before :each do
@t = Thread.new { }
end

after :each do
@t.join
end

it 'converts String keys to Symbol' do
@t.thread_variable_set(:a, 42)
@t.thread_variable_get('a').should == 42
end

it 'tries to convert the key that is neither String nor Symbol with #to_str' do
key = mock('key')
key.should_receive(:to_str).and_return('a')
@t.thread_variable_set(:a, 42)
@t.thread_variable_get(key).should == 42
end

it 'does not raise a TypeError if the key is not a Symbol' do
@t.thread_variable_get(123).should be_nil
end

it 'does not try to convert the key with #to_sym' do
key = mock('key')
key.should_not_receive(:to_sym)
@t.thread_variable_get(key).should be_nil
end
end

describe 'Thread#thread_variable_set' do
before :each do
@t = Thread.new { }
end

after :each do
@t.join
end

it 'converts String keys to Symbol' do
@t.thread_variable_set('a', 42)
@t.thread_variable_get(:a).should == 42
end

it 'tries to convert the key that is neither String nor Symbol with #to_str' do
key = mock('key')
key.should_receive(:to_str).and_return('a')
@t.thread_variable_set(key, 42)
@t.thread_variable_get(:a).should == 42
end

it 'raises a FrozenError if the thread is frozen' do
@t.freeze
-> { @t.thread_variable_set(:a, 1) }.should raise_error(FrozenError, "can't modify frozen thread locals")
end

it 'raises a TypeError if the key is not a Symbol' do
-> { @t.thread_variable_set(123, 1) }.should raise_error(TypeError, '123 is not a symbol')
end

it 'does not try to convert the key with #to_sym' do
key = mock('key')
key.should_not_receive(:to_sym)
-> { @t.thread_variable_set(key, 42) }.should raise_error(TypeError, "#{key.inspect} is not a symbol")
end
end
end
Loading