Skip to content

Commit ce19ce4

Browse files
committed
Release the C extension lock on rb_funcall()
* See socketry/io-event#2
1 parent 6f165d2 commit ce19ce4

File tree

8 files changed

+147
-34
lines changed

8 files changed

+147
-34
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Bug fixes:
1111
* Fix the `--backtraces-raise` and `--backtraces-rescue` options in JVM mode (#2335).
1212
* Fix `File.{atime, mtime, ctime}` to include nanoseconds (#2337).
1313
* Fix `Array#[a, b] = "frozen string literal".freeze` (#2355).
14+
* `rb_funcall()` now releases the C-extension lock (similar to MRI).
1415

1516
Compatibility:
1617

lib/truffle/truffle/cext.rb

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -866,11 +866,11 @@ def rb_cmpint(val, a, b)
866866
end
867867

868868
def rb_funcall_with_block(recv, meth, args, block)
869-
recv.public_send(meth, *args, &block)
869+
Primitive.public_send_without_cext_lock(recv, meth, args, block)
870870
end
871871

872872
def rb_funcallv_public(recv, meth, args)
873-
recv.public_send(meth, *args)
873+
Primitive.public_send_without_cext_lock(recv, meth, args, nil)
874874
end
875875

876876
def rb_funcallv(recv, meth, args)
@@ -881,13 +881,15 @@ def rb_funcall(recv, meth, n, *args)
881881
# see #call_with_thread_locally_stored_block
882882
thread_local_block = Thread.current[:__C_BLOCK__]
883883
Thread.current[:__C_BLOCK__] = nil
884-
recv.__send__(meth, *args, &thread_local_block)
885-
ensure
886-
Thread.current[:__C_BLOCK__] = thread_local_block
884+
begin
885+
Primitive.send_without_cext_lock(recv, meth, args, thread_local_block)
886+
ensure
887+
Thread.current[:__C_BLOCK__] = thread_local_block
888+
end
887889
end
888890

889891
def rb_apply(recv, meth, args)
890-
recv.__send__(meth, *args)
892+
Primitive.send_without_cext_lock(recv, meth, args, nil)
891893
end
892894

893895
def rb_define_attr(klass, name, read, write)
@@ -1645,9 +1647,11 @@ def rb_thread_call_with_gvl(function, data)
16451647
end
16461648

16471649
def rb_thread_call_without_gvl(function, data1, unblock, data2)
1648-
Primitive.call_without_c_mutex(-> {
1649-
Primitive.call_with_unblocking_function(Thread.current, function, data1, unblock, data2)
1650-
}, [])
1650+
Primitive.send_without_cext_lock(self, :rb_thread_call_without_gvl_inner, [function, data1, unblock, data2], nil)
1651+
end
1652+
1653+
private def rb_thread_call_without_gvl_inner(function, data1, unblock, data2)
1654+
Primitive.call_with_unblocking_function(Thread.current, function, data1, unblock, data2)
16511655
end
16521656

16531657
def rb_iterate(iteration, iterated_object, callback, callback_arg)
@@ -1672,14 +1676,14 @@ def rb_iterate(iteration, iterated_object, callback, callback_arg)
16721676
def rb_thread_wait_fd(fd)
16731677
io = IO.for_fd(fd)
16741678
io.autoclose = false
1675-
Primitive.call_without_c_mutex(IO.method(:select), [[io]])
1679+
Primitive.send_without_cext_lock(IO, :select, [[io]], nil)
16761680
nil
16771681
end
16781682

16791683
def rb_thread_fd_writable(fd)
16801684
io = IO.for_fd(fd)
16811685
io.autoclose = false
1682-
_r, w, _e = Primitive.call_without_c_mutex(IO.method(:select), [nil, [io]])
1686+
_r, w, _e = Primitive.send_without_cext_lock(IO, :select, [nil, [io]], nil)
16831687
w.size
16841688
end
16851689

@@ -1698,7 +1702,7 @@ def rb_wait_for_single_fd(fd, events, tv_secs, tv_usecs)
16981702
if tv_secs >= 0 || tv_usecs >= 0
16991703
timeout = tv_secs + tv_usecs/1.0e6
17001704
end
1701-
r, w, e = Primitive.call_without_c_mutex(IO.method(:select), [read, write, error, *timeout])
1705+
r, w, e = Primitive.send_without_cext_lock(IO, :select, [read, write, error, *timeout], nil)
17021706
if r.nil? # timeout
17031707
0
17041708
else

spec/truffle/capi/cext_lock_spec.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved. This
2+
# code is released under a tri EPL/GPL/LGPL license. You can use it,
3+
# redistribute it and/or modify it under the terms of the:
4+
#
5+
# Eclipse Public License version 2.0, or
6+
# GNU General Public License version 2, or
7+
# GNU Lesser General Public License version 2.1.
8+
9+
require_relative '../../ruby/optional/capi/spec_helper'
10+
11+
load_extension("truffleruby_lock")
12+
13+
describe "TruffleRuby C-ext lock" do
14+
before :each do
15+
@t = CApiTruffleRubyLockSpecs.new
16+
end
17+
18+
it "is not acquired in Ruby code" do
19+
Truffle::CExt.cext_lock_owned?.should == false
20+
end
21+
22+
guard -> { Truffle::Boot.get_option 'cexts-lock' } do
23+
it "is acquired in C ext code" do
24+
@t.has_lock?.should == true
25+
end
26+
end
27+
28+
it "is released inside rb_thread_call_without_gvl" do
29+
@t.has_lock_in_call_without_gvl?.should == false
30+
end
31+
32+
it "is released inside rb_funcall" do
33+
@t.has_lock_in_rb_funcall?(Truffle::CExt).should == false
34+
end
35+
end
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved. This
3+
* code is released under a tri EPL/GPL/LGPL license. You can use it,
4+
* redistribute it and/or modify it under the terms of the:
5+
*
6+
* Eclipse Public License version 2.0, or
7+
* GNU General Public License version 2, or
8+
* GNU Lesser General Public License version 2.1.
9+
*/
10+
#include "ruby.h"
11+
#include "ruby/thread.h"
12+
#include "rubyspec.h"
13+
14+
#ifdef __cplusplus
15+
extern "C" {
16+
#endif
17+
18+
static VALUE has_lock(VALUE self) {
19+
return RUBY_CEXT_FUNCALL("cext_lock_owned?");
20+
}
21+
22+
static void* called_without_gvl(void* data) {
23+
return RUBY_CEXT_FUNCALL("cext_lock_owned?");
24+
}
25+
26+
static VALUE has_lock_in_call_without_gvl(VALUE self) {
27+
return rb_thread_call_without_gvl(called_without_gvl, 0, RUBY_UBF_IO, 0);
28+
}
29+
30+
static VALUE has_lock_in_rb_funcall(VALUE self, VALUE truffleCExt) {
31+
return rb_funcall(truffleCExt, rb_intern("cext_lock_owned?"), 0);
32+
}
33+
34+
void Init_truffleruby_lock_spec(void) {
35+
VALUE cls = rb_define_class("CApiTruffleRubyLockSpecs", rb_cObject);
36+
rb_define_method(cls, "has_lock?", has_lock, 0);
37+
rb_define_method(cls, "has_lock_in_call_without_gvl?", has_lock_in_call_without_gvl, 0);
38+
rb_define_method(cls, "has_lock_in_rb_funcall?", has_lock_in_rb_funcall, 1);
39+
}
40+
41+
#ifdef __cplusplus
42+
}
43+
#endif

spec/truffle/capi/rbasic_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
# GNU Lesser General Public License version 2.1.
88
#
99
require_relative '../../ruby/optional/capi/spec_helper'
10+
1011
load_extension("truffleruby_rbasic")
1112

1213
describe "RBasic support" do

src/main/java/org/truffleruby/cext/CExtNodes.java

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
import org.truffleruby.builtins.Primitive;
2626
import org.truffleruby.builtins.PrimitiveArrayArgumentsNode;
2727
import org.truffleruby.builtins.YieldingCoreMethodNode;
28-
import org.truffleruby.cext.CExtNodesFactory.CallCWithMutexNodeFactory;
28+
import org.truffleruby.cext.CExtNodesFactory.CallWithCExtLockNodeFactory;
2929
import org.truffleruby.cext.CExtNodesFactory.StringToNativeNodeGen;
3030
import org.truffleruby.collections.ConcurrentOperations;
3131
import org.truffleruby.core.CoreLibrary;
@@ -118,16 +118,15 @@
118118
public class CExtNodes {
119119

120120
@Primitive(name = "call_with_c_mutex_and_frame")
121-
public abstract static class CallCWithMuteAndFramexNode extends PrimitiveArrayArgumentsNode {
121+
public abstract static class CallWithCExtLockAndFrameNode extends PrimitiveArrayArgumentsNode {
122122

123-
@Child protected CallCWithMutexNode callCextNode = CallCWithMutexNodeFactory.create(RubyNode.EMPTY_ARRAY);
123+
@Child protected CallWithCExtLockNode callCextNode = CallWithCExtLockNodeFactory.create(RubyNode.EMPTY_ARRAY);
124124

125125
@Specialization
126-
protected Object callCWithMutex(Object receiver, RubyArray argsArray, Object block,
126+
protected Object callWithCExtLockAndFrame(Object receiver, RubyArray argsArray, Object block,
127127
@Cached MarkingServiceNodes.GetMarkerThreadLocalDataNode getDataNode) {
128-
ExtensionCallStack extensionStack = getDataNode.execute().getExtensionCallStack();
128+
final ExtensionCallStack extensionStack = getDataNode.execute().getExtensionCallStack();
129129
extensionStack.push(block);
130-
131130
try {
132131
return callCextNode.execute(receiver, argsArray);
133132
} finally {
@@ -137,12 +136,12 @@ protected Object callCWithMutex(Object receiver, RubyArray argsArray, Object blo
137136
}
138137

139138
@Primitive(name = "call_with_c_mutex")
140-
public abstract static class CallCWithMutexNode extends PrimitiveArrayArgumentsNode {
139+
public abstract static class CallWithCExtLockNode extends PrimitiveArrayArgumentsNode {
141140

142141
public abstract Object execute(Object receiver, RubyArray argsArray);
143142

144143
@Specialization(limit = "getCacheLimit()")
145-
protected Object callCWithMutex(Object receiver, RubyArray argsArray,
144+
protected Object callWithCExtLock(Object receiver, RubyArray argsArray,
146145
@CachedLibrary("receiver") InteropLibrary receivers,
147146
@Cached ArrayToObjectArrayNode arrayToObjectArrayNode,
148147
@Cached TranslateInteropExceptionNode translateInteropExceptionNode,
@@ -174,14 +173,12 @@ protected int getCacheLimit() {
174173
}
175174
}
176175

177-
@Primitive(name = "call_without_c_mutex")
178-
public abstract static class CallCWithoutMutexNode extends PrimitiveArrayArgumentsNode {
179-
180-
@Specialization(limit = "getCacheLimit()")
181-
protected Object callCWithoutMutex(Object receiver, RubyArray argsArray,
176+
@Primitive(name = "send_without_cext_lock")
177+
public abstract static class SendWithoutCExtLockNode extends PrimitiveArrayArgumentsNode {
178+
@Specialization
179+
protected Object sendWithoutCExtLock(Object receiver, RubySymbol method, RubyArray argsArray, Object block,
182180
@Cached ArrayToObjectArrayNode arrayToObjectArrayNode,
183-
@CachedLibrary("receiver") InteropLibrary receivers,
184-
@Cached TranslateInteropExceptionNode translateInteropExceptionNode,
181+
@Cached DispatchNode dispatchNode,
185182
@Cached ConditionProfile ownedProfile) {
186183
final Object[] args = arrayToObjectArrayNode.executeToObjectArray(argsArray);
187184

@@ -193,22 +190,54 @@ protected Object callCWithoutMutex(Object receiver, RubyArray argsArray,
193190
MutexOperations.unlockInternal(lock);
194191
}
195192
try {
196-
return InteropNodes.execute(receiver, args, receivers, translateInteropExceptionNode);
193+
return dispatchNode.callWithBlock(receiver, method.getString(), block, args);
197194
} finally {
198195
if (owned) {
199196
MutexOperations.internalLockEvenWithException(getContext(), lock, this);
200197
}
201198
}
202199
} else {
203-
return InteropNodes.execute(receiver, args, receivers, translateInteropExceptionNode);
200+
return dispatchNode.callWithBlock(receiver, method.getString(), block, args);
204201
}
205-
206202
}
203+
}
207204

208-
protected int getCacheLimit() {
209-
return getLanguage().options.DISPATCH_CACHE;
205+
@Primitive(name = "public_send_without_cext_lock")
206+
public abstract static class PublicSendWithoutCExtLockNode extends PrimitiveArrayArgumentsNode {
207+
@Specialization
208+
protected Object publicSendWithoutLock(Object receiver, RubySymbol method, RubyArray argsArray, Object block,
209+
@Cached ArrayToObjectArrayNode arrayToObjectArrayNode,
210+
@Cached(parameters = "PUBLIC") DispatchNode dispatchNode,
211+
@Cached ConditionProfile ownedProfile) {
212+
final Object[] args = arrayToObjectArrayNode.executeToObjectArray(argsArray);
213+
214+
if (getContext().getOptions().CEXT_LOCK) {
215+
final ReentrantLock lock = getContext().getCExtensionsLock();
216+
boolean owned = ownedProfile.profile(lock.isHeldByCurrentThread());
217+
218+
if (owned) {
219+
MutexOperations.unlockInternal(lock);
220+
}
221+
try {
222+
return dispatchNode.callWithBlock(receiver, method.getString(), block, args);
223+
} finally {
224+
if (owned) {
225+
MutexOperations.internalLockEvenWithException(getContext(), lock, this);
226+
}
227+
}
228+
} else {
229+
return dispatchNode.callWithBlock(receiver, method.getString(), block, args);
230+
}
210231
}
232+
}
211233

234+
@CoreMethod(names = "cext_lock_owned?", onSingleton = true)
235+
public abstract static class IsCExtLockOwnedNode extends CoreMethodArrayArgumentsNode {
236+
@Specialization
237+
protected boolean isCExtLockOwned() {
238+
final ReentrantLock lock = getContext().getCExtensionsLock();
239+
return lock.isHeldByCurrentThread();
240+
}
212241
}
213242

214243
@CoreMethod(names = "rb_ulong2num", onSingleton = true, required = 1)

src/main/java/org/truffleruby/language/ReadOwnFrameNode.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
public class ReadOwnFrameNode extends RubyBaseNode implements FrameOrVariablesReadingNode {
1515

1616
public Object execute(Frame frame) {
17-
return frame.materialize();
17+
return frame != null ? frame.materialize() : null;
1818
}
1919

2020
@Override

test/truffle/cexts/backtraces/bin/backtraces

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def callback
2020
end
2121

2222
def ruby_call
23-
RB_FUNCALLV.call(Truffle::CExt.test_cext_wrap(self), Truffle::CExt.test_cext_wrap('callback'), 0, nil)
23+
RB_FUNCALLV.call(Truffle::CExt.test_cext_wrap(self), Truffle::CExt.test_cext_wrap(:callback), 0, nil)
2424
end
2525

2626
def top

0 commit comments

Comments
 (0)