Skip to content

Commit 8e49cf3

Browse files
committed
WIP: Use Primitive.use_cext_lock? to find out if the C ext lock should be used
1 parent 597cbc8 commit 8e49cf3

File tree

5 files changed

+79
-29
lines changed

5 files changed

+79
-29
lines changed

doc/user/thread-safe-extensions.md

+22-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ permalink: /reference-manual/ruby/ThreadSafeExtensions/
88

99
Native extensions are by default considered thread-unsafe for maximum compatibility with CRuby and use the global extension lock (unless `--cexts-lock=false` is used).
1010

11-
Extensions can mark themselves as thread-safe either by using `rb_ext_ractor_safe()` or `rb_ext_thread_safe()` (TruffleRuby-specific).
11+
Extensions can mark themselves as thread-safe either by using `rb_ext_ractor_safe()` or `rb_ext_thread_safe()` (the latter is TruffleRuby-specific).
1212
Such extensions are then run by TruffleRuby without a global extension lock, i.e. in parallel.
1313

1414
Here is an example of an extension marking itself as Ractor-safe.
@@ -34,6 +34,27 @@ void Init_my_extension(void) {
3434
}
3535
```
3636

37+
It is possible to mark individual methods as Ractor/Thread-safe by using `rb_ext_ractor_safe/rb_ext_thread_safe(true/false)` around them (these functions actually set a Fiber-local flag):
38+
```c
39+
void Init_my_extension(void) {
40+
#ifdef HAVE_RB_EXT_RACTOR_SAFE
41+
rb_ext_ractor_safe(true);
42+
#endif
43+
rb_define_method(myClass, "ractor_safe_method", foo_impl, 0); // The C function foo_impl can be called from multiple threads in parallel
44+
// more Ractor-safe methods
45+
46+
#ifdef HAVE_RB_EXT_RACTOR_SAFE
47+
rb_ext_ractor_safe(false);
48+
#endif
49+
rb_define_method(myClass, "ractor_unsafe_method", bar_impl, 0); // The C function bar_impl needs a global extension lock for correctness
50+
// more Ractor-unsafe methods
51+
}
52+
```
53+
54+
Other Ruby C API functions taking a C function like `rb_proc_new()` do not use the global extension lock if:
55+
* Called inside the `Init_my_extension` and `rb_ext_ractor_safe(true)` / `rb_ext_thread_safe(true)` are used.
56+
* Called outside the `Init_my_extension` and the calling function does not hold the global extension lock.
57+
3758
The conditions for an extension to be thread-safe are the following.
3859
This is similar to [the conditions for Ractor-safe extensions](https://github.com/ruby/ruby/blob/master/doc/extension.rdoc#appendix-f-ractor-support-) but not all conditions are necessary.
3960
1. The extension should make it clear in its documentation which objects are safe to share between threads and which are not.

lib/truffle/truffle/cext.rb

+33-14
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ module Truffle::CExt
2727
SULONG = Truffle::Boot.get_option('cexts-sulong')
2828
NFI = !SULONG
2929

30+
CEXT_LOCK = Truffle::Boot.get_option('cexts-lock')
31+
3032
SET_LIBTRUFFLERUBY = -> libtruffleruby do
3133
LIBTRUFFLERUBY = libtruffleruby
3234
end
@@ -243,7 +245,7 @@ def init_extension(library, library_path)
243245
# Resolve while inside the ExtensionCallStackEntry to ensure the preservedObjects are still all alive
244246
resolve_registered_addresses
245247
end
246-
}, [], nil, nil)
248+
}, [], nil, nil, CEXT_LOCK)
247249
end
248250

249251
def supported?
@@ -856,12 +858,14 @@ def rb_str_new_frozen(value)
856858
end
857859

858860
def rb_tracepoint_new(events, func, data)
861+
use_cext_lock = Primitive.use_cext_lock?
859862
TracePoint.new(*events_to_events_array(events)) do |tp|
860863
Primitive.call_with_cext_lock_and_frame(
861864
POINTER2_TO_VOID_WRAPPER,
862865
[func, Primitive.cext_wrap(tp), data],
863866
Primitive.caller_special_variables_if_available,
864-
nil)
867+
nil,
868+
use_cext_lock)
865869
end
866870
end
867871

@@ -1204,6 +1208,7 @@ def rb_path_to_class(path)
12041208
end
12051209

12061210
def rb_proc_new(function, value)
1211+
use_cext_lock = Primitive.use_cext_lock?
12071212
Proc.new do |*args, &block|
12081213
Primitive.call_with_cext_lock_and_frame_and_unwrap(RB_BLOCK_CALL_FUNC_WRAPPER, [
12091214
function,
@@ -1212,7 +1217,7 @@ def rb_proc_new(function, value)
12121217
args.size, # argc
12131218
Truffle::CExt.RARRAY_PTR(args), # argv
12141219
Primitive.cext_wrap(block), # blockarg
1215-
], Primitive.caller_special_variables_if_available, nil)
1220+
], Primitive.caller_special_variables_if_available, nil, use_cext_lock)
12161221
end
12171222
end
12181223

@@ -1475,12 +1480,14 @@ def rb_enumeratorize(obj, meth, args)
14751480

14761481
def rb_enumeratorize_with_size(obj, meth, args, size_fn)
14771482
return rb_enumeratorize(obj, meth, args) if Primitive.interop_null?(size_fn)
1483+
use_cext_lock = Primitive.use_cext_lock?
14781484
enum = obj.to_enum(meth, *args) do
14791485
Primitive.call_with_cext_lock_and_frame_and_unwrap(
14801486
POINTER3_TO_POINTER_WRAPPER,
14811487
[size_fn, Primitive.cext_wrap(obj), Primitive.cext_wrap(args), Primitive.cext_wrap(enum)],
14821488
Primitive.caller_special_variables_if_available,
1483-
nil)
1489+
nil,
1490+
use_cext_lock)
14841491
end
14851492
enum
14861493
end
@@ -1494,12 +1501,14 @@ def rb_newobj_of(ruby_class)
14941501
end
14951502

14961503
def rb_define_alloc_func(ruby_class, function)
1504+
use_cext_lock = Primitive.use_cext_lock?
14971505
ruby_class.singleton_class.define_method(:__allocate__) do
14981506
Primitive.call_with_cext_lock_and_frame_and_unwrap(
14991507
POINTER_TO_POINTER_WRAPPER,
15001508
[function, Primitive.cext_wrap(self)],
15011509
Primitive.caller_special_variables_if_available,
1502-
nil)
1510+
nil,
1511+
use_cext_lock)
15031512
end
15041513
class << ruby_class
15051514
private :__allocate__
@@ -1697,10 +1706,11 @@ def rb_nativethread_lock_destroy(lock)
16971706
end
16981707

16991708
def rb_set_end_proc(func, data)
1709+
use_cext_lock = Primitive.use_cext_lock?
17001710
at_exit do
17011711
Primitive.call_with_cext_lock_and_frame(
17021712
POINTER_TO_VOID_WRAPPER, [func, data],
1703-
Primitive.caller_special_variables_if_available, nil)
1713+
Primitive.caller_special_variables_if_available, nil, use_cext_lock)
17041714
end
17051715
end
17061716

@@ -1736,10 +1746,11 @@ def RTYPEDDATA(object)
17361746

17371747
private def data_sizer(sizer_function, rtypeddata)
17381748
raise unless sizer_function.respond_to?(:call)
1749+
use_cext_lock = Primitive.use_cext_lock?
17391750
proc {
17401751
Primitive.call_with_cext_lock_and_frame(
17411752
POINTER_TO_SIZE_T_WRAPPER, [sizer_function, rtypeddata],
1742-
Primitive.caller_special_variables_if_available, nil)
1753+
Primitive.caller_special_variables_if_available, nil, use_cext_lock)
17431754
}
17441755
end
17451756

@@ -1779,7 +1790,9 @@ def rb_data_typed_object_wrap(ruby_class, data, data_type, mark, free, size)
17791790
end
17801791

17811792
def run_data_finalizer(function, data)
1782-
Primitive.call_with_cext_lock_and_frame POINTER_TO_VOID_WRAPPER, [function, data], nil, nil
1793+
# TODO: Maybe we should not hold the C extension lock here, and instead save the result of Primitive.use_cext_lock?
1794+
# in DataObjectFinalizerReference from rb_data_object_wrap/rb_data_typed_object_wrap.
1795+
Primitive.call_with_cext_lock_and_frame POINTER_TO_VOID_WRAPPER, [function, data], nil, nil, CEXT_LOCK
17831796
end
17841797

17851798
def run_marker(obj)
@@ -2006,8 +2019,9 @@ def rb_time_interval_acceptable(time_val)
20062019
end
20072020

20082021
def rb_thread_create(fn, args)
2022+
use_cext_lock = Primitive.use_cext_lock?
20092023
Thread.new do
2010-
Primitive.call_with_cext_lock_and_frame(POINTER_TO_POINTER_WRAPPER, [fn, args], Primitive.caller_special_variables_if_available, nil)
2024+
Primitive.call_with_cext_lock_and_frame(POINTER_TO_POINTER_WRAPPER, [fn, args], Primitive.caller_special_variables_if_available, nil, use_cext_lock)
20112025
end
20122026
end
20132027

@@ -2035,6 +2049,7 @@ def rb_thread_call_without_gvl(function, data1, unblock, data2)
20352049
end
20362050

20372051
def rb_iterate(iteration, iterated_object, callback, callback_arg)
2052+
use_cext_lock = Primitive.use_cext_lock?
20382053
block = rb_block_proc
20392054
wrapped_callback = proc do |block_arg|
20402055
Primitive.call_with_cext_lock_and_frame_and_unwrap(RB_BLOCK_CALL_FUNC_WRAPPER, [
@@ -2044,13 +2059,13 @@ def rb_iterate(iteration, iterated_object, callback, callback_arg)
20442059
0, # argc
20452060
nil, # argv
20462061
nil, # blockarg
2047-
], Primitive.cext_special_variables_from_stack, block)
2062+
], Primitive.cext_special_variables_from_stack, block, use_cext_lock)
20482063
end
20492064
Primitive.cext_unwrap(
20502065
Primitive.call_with_cext_lock_and_frame(POINTER_TO_POINTER_WRAPPER, [
20512066
iteration,
20522067
Primitive.cext_wrap(iterated_object)
2053-
], Primitive.cext_special_variables_from_stack, wrapped_callback))
2068+
], Primitive.cext_special_variables_from_stack, wrapped_callback, use_cext_lock))
20542069
end
20552070

20562071
# From ruby.h
@@ -2147,21 +2162,24 @@ def rb_hash_aref(object, key)
21472162
def rb_define_hooked_variable(name, gvar, getter, setter)
21482163
name = "$#{name}" unless name.start_with?('$')
21492164
id = name.to_sym
2165+
use_cext_lock = Primitive.use_cext_lock?
21502166

21512167
getter_proc = -> {
21522168
Primitive.call_with_cext_lock_and_frame_and_unwrap(
21532169
POINTER2_TO_POINTER_WRAPPER,
21542170
[getter, Primitive.cext_wrap(id), gvar],
21552171
Primitive.caller_special_variables_if_available,
2156-
nil)
2172+
nil,
2173+
use_cext_lock)
21572174
}
21582175

21592176
setter_proc = -> value {
21602177
Primitive.call_with_cext_lock_and_frame(
21612178
POINTER3_TO_VOID_WRAPPER,
21622179
[setter, Primitive.cext_wrap(value), Primitive.cext_wrap(id), gvar],
21632180
Primitive.caller_special_variables_if_available,
2164-
nil)
2181+
nil,
2182+
use_cext_lock)
21652183
}
21662184

21672185
Truffle::KernelOperations.define_hooked_variable id, getter_proc, setter_proc
@@ -2285,6 +2303,7 @@ def rb_fiber_current
22852303
end
22862304

22872305
def rb_fiber_new(function, value)
2306+
use_cext_lock = Primitive.use_cext_lock?
22882307
Fiber.new do |*args|
22892308
Primitive.call_with_cext_lock_and_frame_and_unwrap(RB_BLOCK_CALL_FUNC_WRAPPER, [
22902309
function,
@@ -2293,7 +2312,7 @@ def rb_fiber_new(function, value)
22932312
0, # argc
22942313
nil, # argv
22952314
nil, # blockarg
2296-
], nil, nil)
2315+
], nil, nil, use_cext_lock)
22972316
end
22982317
end
22992318

lib/truffle/truffle/cext_ruby.rb

+4-4
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def rb_define_method(mod, name, function, argc)
2121
end
2222

2323
wrapper = RB_DEFINE_METHOD_WRAPPERS[argc]
24-
thread_safe = Primitive.cext_thread_safe?
24+
use_cext_lock = Primitive.use_cext_lock?
2525
method_body = Truffle::Graal.copy_captured_locals -> *args, &block do
2626
if argc == -1 # (int argc, VALUE *argv, VALUE obj)
2727
args = [function, args.size, Truffle::CExt.RARRAY_PTR(args), Primitive.cext_wrap(self)]
@@ -36,10 +36,10 @@ def rb_define_method(mod, name, function, argc)
3636

3737
# We must set block argument if given here so that the
3838
# `rb_block_*` functions will be able to find it by walking the stack.
39-
if thread_safe
40-
Primitive.call_with_frame_and_unwrap(wrapper, args, Primitive.caller_special_variables_if_available, block)
41-
else
39+
if use_cext_lock
4240
Primitive.call_with_cext_lock_and_frame_and_unwrap(wrapper, args, Primitive.caller_special_variables_if_available, block)
41+
else
42+
Primitive.call_with_frame_and_unwrap(wrapper, args, Primitive.caller_special_variables_if_available, block)
4343
end
4444
end
4545

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

+19-10
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,12 @@ public abstract static class CallWithCExtLockAndFrameNode extends PrimitiveArray
183183

184184
@Specialization
185185
Object callWithCExtLockAndFrame(
186-
VirtualFrame frame, Object receiver, RubyArray argsArray, Object specialVariables, Object block,
186+
VirtualFrame frame,
187+
Object receiver,
188+
RubyArray argsArray,
189+
Object specialVariables,
190+
Object block,
191+
boolean useCExtLock,
187192
@CachedLibrary(limit = "getCacheLimit()") InteropLibrary receivers,
188193
@Cached ArrayToObjectArrayNode arrayToObjectArrayNode,
189194
@Cached TranslateInteropExceptionNode translateInteropExceptionNode,
@@ -197,7 +202,7 @@ Object callWithCExtLockAndFrame(
197202
try {
198203
final Object[] args = arrayToObjectArrayNode.executeToObjectArray(argsArray);
199204

200-
if (getContext().getOptions().CEXT_LOCK) {
205+
if (getContext().getOptions().CEXT_LOCK && useCExtLock) {
201206
final ReentrantLock lock = getContext().getCExtensionsLock();
202207
boolean owned = ownedProfile.profile(this, lock.isHeldByCurrentThread());
203208

@@ -499,6 +504,18 @@ boolean isCExtLockOwned() {
499504
}
500505
}
501506

507+
@CoreMethod(names = "use_cext_lock?", onSingleton = true)
508+
public abstract static class UseCExtLockNode extends PrimitiveArrayArgumentsNode {
509+
@Specialization
510+
boolean useCExtLock() {
511+
if (getLanguage().getCurrentFiber().threadSafeExtension) {
512+
return false;
513+
} else {
514+
return getContext().getCExtensionsLock().isHeldByCurrentThread();
515+
}
516+
}
517+
}
518+
502519
@CoreMethod(names = "rb_ulong2num", onSingleton = true, required = 1)
503520
public abstract static class ULong2NumNode extends CoreMethodArrayArgumentsNode {
504521

@@ -2147,12 +2164,4 @@ Object setThreadSafe(int threadSafe) {
21472164
}
21482165
}
21492166

2150-
@Primitive(name = "cext_thread_safe?")
2151-
public abstract static class CExtIsThreadSafe extends PrimitiveArrayArgumentsNode {
2152-
@Specialization
2153-
boolean isThreadSafe() {
2154-
return getLanguage().getCurrentFiber().threadSafeExtension;
2155-
}
2156-
}
2157-
21582167
}

src/main/java/org/truffleruby/core/fiber/RubyFiber.java

+1
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ public enum FiberStatus {
104104
public final ValueWrapperManager.HandleBlockHolder handleData;
105105
boolean blocking = true;
106106
public RubyArray cGlobalVariablesDuringInitFunction;
107+
/** Always false when not inside a C ext Init_ function */
107108
public boolean threadSafeExtension = false;
108109

109110
// To pass state between beforeEnter(), fiberMain() and afterLeave()

0 commit comments

Comments
 (0)