atomic 1.0.2 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/.travis.yml +9 -0
- data/README.md +9 -3
- data/Rakefile +12 -0
- data/atomic.gemspec +1 -1
- data/ext/atomic_reference.c +36 -31
- data/ext/org/jruby/ext/atomic/AtomicReferenceLibrary.java +58 -20
- data/lib/atomic.rb +3 -100
- data/lib/atomic/concurrent_update_error.rb +6 -0
- data/lib/atomic/delegated_update.rb +25 -0
- data/lib/atomic/direct_update.rb +25 -0
- data/lib/atomic/fallback.rb +43 -0
- data/lib/atomic/jruby.rb +2 -0
- data/lib/atomic/rbx.rb +11 -0
- data/lib/atomic/ruby.rb +2 -0
- metadata +10 -2
data/.gitignore
CHANGED
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,5 +1,4 @@
|
|
1
|
-
atomic: An atomic reference implementation for JRuby and
|
2
|
-
Ruby implementations (MRI 1.8/1.9, Rubinius)
|
1
|
+
atomic: An atomic reference implementation for JRuby, Rubinius, and MRI.
|
3
2
|
|
4
3
|
Summary
|
5
4
|
=======
|
@@ -56,4 +55,11 @@ my_atomic.value = 1
|
|
56
55
|
my_atomic.swap(2) # => 1
|
57
56
|
my_atomic.compare_and_swap(2, 3) # => true, updated to 3
|
58
57
|
my_atomic.compare_and_swap(2, 3) # => false, current is not 2
|
59
|
-
````
|
58
|
+
````
|
59
|
+
|
60
|
+
Building
|
61
|
+
========
|
62
|
+
|
63
|
+
As of 1.1.0, JDK8 is required to build the atomic gem, since it attempts to use
|
64
|
+
the new atomic Unsafe.getAndSetObject method only in JDK8. The resulting code
|
65
|
+
should still work fine as far back as Java 5.
|
data/Rakefile
CHANGED
@@ -18,6 +18,7 @@ task :default => :test
|
|
18
18
|
desc "Run tests"
|
19
19
|
Rake::TestTask.new :test do |t|
|
20
20
|
t.libs << "lib"
|
21
|
+
t.libs << "ext"
|
21
22
|
t.test_files = FileList["test/**/*.rb"]
|
22
23
|
end
|
23
24
|
|
@@ -50,4 +51,15 @@ if defined?(JRUBY_VERSION)
|
|
50
51
|
end
|
51
52
|
|
52
53
|
task :package => :jar
|
54
|
+
else
|
55
|
+
task :package do
|
56
|
+
Dir.chdir("ext") do
|
57
|
+
# this does essentially the same thing
|
58
|
+
# as what RubyGems does
|
59
|
+
ruby "extconf.rb"
|
60
|
+
sh "make"
|
61
|
+
end
|
62
|
+
end
|
53
63
|
end
|
64
|
+
|
65
|
+
task :test => :package
|
data/atomic.gemspec
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = %q{atomic}
|
5
|
-
s.version = "1.0
|
5
|
+
s.version = "1.1.0"
|
6
6
|
s.authors = ["Charles Oliver Nutter", "MenTaLguY"]
|
7
7
|
s.date = Time.now.strftime('%Y-%m-%d')
|
8
8
|
s.description = "An atomic reference implementation for JRuby, Rubinius, and MRI"
|
data/ext/atomic_reference.c
CHANGED
@@ -13,62 +13,67 @@
|
|
13
13
|
#include <ruby.h>
|
14
14
|
|
15
15
|
static void ir_mark(void *value) {
|
16
|
-
|
16
|
+
rb_gc_mark_maybe((VALUE) value);
|
17
17
|
}
|
18
18
|
|
19
19
|
static VALUE ir_alloc(VALUE klass) {
|
20
|
-
|
20
|
+
return rb_data_object_alloc(klass, (void *) Qnil, ir_mark, NULL);
|
21
21
|
}
|
22
22
|
|
23
|
-
static VALUE ir_initialize(VALUE
|
24
|
-
|
25
|
-
|
23
|
+
static VALUE ir_initialize(int argc, VALUE* argv, VALUE self) {
|
24
|
+
VALUE value = Qnil;
|
25
|
+
if (rb_scan_args(argc, argv, "01", &value) == 1) {
|
26
|
+
value = argv[0];
|
27
|
+
}
|
28
|
+
DATA_PTR(self) = (void *) value;
|
29
|
+
return Qnil;
|
26
30
|
}
|
27
31
|
|
28
32
|
static VALUE ir_get(VALUE self) {
|
29
|
-
|
33
|
+
return (VALUE) DATA_PTR(self);
|
30
34
|
}
|
31
35
|
|
32
36
|
static VALUE ir_set(VALUE self, VALUE new_value) {
|
33
|
-
|
34
|
-
|
37
|
+
DATA_PTR(self) = (void *) new_value;
|
38
|
+
return new_value;
|
35
39
|
}
|
36
40
|
|
37
41
|
static VALUE ir_get_and_set(VALUE self, VALUE new_value) {
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
+
VALUE old_value;
|
43
|
+
old_value = (VALUE) DATA_PTR(self);
|
44
|
+
DATA_PTR(self) = (void *) new_value;
|
45
|
+
return old_value;
|
42
46
|
}
|
43
47
|
|
44
48
|
static VALUE ir_compare_and_set(volatile VALUE self, VALUE expect_value, VALUE new_value) {
|
45
49
|
#if __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ >= 1050
|
46
|
-
|
47
|
-
|
48
|
-
|
50
|
+
if (OSAtomicCompareAndSwap64(expect_value, new_value, &DATA_PTR(self))) {
|
51
|
+
return Qtrue;
|
52
|
+
}
|
49
53
|
#elif (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) > 40100
|
50
|
-
|
51
|
-
|
52
|
-
|
54
|
+
if (__sync_bool_compare_and_swap(&DATA_PTR(self), expect_value, new_value)) {
|
55
|
+
return Qtrue;
|
56
|
+
}
|
53
57
|
#else
|
54
|
-
#
|
58
|
+
#error No CAS operation available for this platform
|
55
59
|
#endif
|
56
|
-
|
60
|
+
return Qfalse;
|
57
61
|
}
|
58
62
|
|
59
63
|
void Init_atomic_reference() {
|
60
|
-
|
61
|
-
VALUE cInternalReference;
|
64
|
+
VALUE cAtomic;
|
62
65
|
|
63
|
-
|
64
|
-
cInternalReference = rb_define_class_under(cAtomic, "InternalReference",
|
65
|
-
rb_cObject);
|
66
|
+
cAtomic = rb_define_class_under(rb_cObject, "Atomic", rb_cObject);
|
66
67
|
|
67
|
-
|
68
|
+
rb_define_alloc_func(cAtomic, ir_alloc);
|
68
69
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
70
|
+
rb_define_method(cAtomic, "initialize", ir_initialize, -1);
|
71
|
+
rb_define_method(cAtomic, "get", ir_get, 0);
|
72
|
+
rb_define_method(cAtomic, "value", ir_get, 0);
|
73
|
+
rb_define_method(cAtomic, "set", ir_set, 1);
|
74
|
+
rb_define_method(cAtomic, "value=", ir_set, 1);
|
75
|
+
rb_define_method(cAtomic, "get_and_set", ir_get_and_set, 1);
|
76
|
+
rb_define_method(cAtomic, "swap", ir_get_and_set, 1);
|
77
|
+
rb_define_method(cAtomic, "compare_and_set", ir_compare_and_set, 2);
|
78
|
+
rb_define_method(cAtomic, "compare_and_swap", ir_compare_and_set, 2);
|
74
79
|
}
|
@@ -24,6 +24,8 @@ import org.jruby.runtime.ObjectAllocator;
|
|
24
24
|
import org.jruby.runtime.ThreadContext;
|
25
25
|
import org.jruby.runtime.builtin.IRubyObject;
|
26
26
|
import org.jruby.runtime.load.Library;
|
27
|
+
import org.jruby.util.unsafe.UnsafeFactory;
|
28
|
+
import org.jruby.util.unsafe.UnsafeGetter;
|
27
29
|
|
28
30
|
/**
|
29
31
|
* This library adds an atomic reference type to JRuby for use in the atomic
|
@@ -34,10 +36,14 @@ import org.jruby.runtime.load.Library;
|
|
34
36
|
*/
|
35
37
|
public class AtomicReferenceLibrary implements Library {
|
36
38
|
public void load(Ruby runtime, boolean wrap) throws IOException {
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
39
|
+
RubyClass atomicCls = runtime.defineClass("Atomic", runtime.getObject(), JRUBYREFERENCE_ALLOCATOR);
|
40
|
+
try {
|
41
|
+
sun.misc.Unsafe.class.getMethod("getAndSetObject", Object.class);
|
42
|
+
atomicCls.setAllocator(JRUBYREFERENCE8_ALLOCATOR);
|
43
|
+
} catch (Exception e) {
|
44
|
+
// leave it as Java 6/7 version
|
45
|
+
}
|
46
|
+
atomicCls.defineAnnotatedMethods(JRubyReference.class);
|
41
47
|
}
|
42
48
|
|
43
49
|
private static final ObjectAllocator JRUBYREFERENCE_ALLOCATOR = new ObjectAllocator() {
|
@@ -45,51 +51,83 @@ public class AtomicReferenceLibrary implements Library {
|
|
45
51
|
return new JRubyReference(runtime, klazz);
|
46
52
|
}
|
47
53
|
};
|
54
|
+
|
55
|
+
private static final ObjectAllocator JRUBYREFERENCE8_ALLOCATOR = new ObjectAllocator() {
|
56
|
+
public IRubyObject allocate(Ruby runtime, RubyClass klazz) {
|
57
|
+
return new JRubyReference(runtime, klazz);
|
58
|
+
}
|
59
|
+
};
|
48
60
|
|
49
61
|
@JRubyClass(name="JRubyReference", parent="Object")
|
50
62
|
public static class JRubyReference extends RubyObject {
|
51
|
-
|
52
|
-
|
53
|
-
|
63
|
+
volatile IRubyObject reference;
|
64
|
+
|
65
|
+
static final sun.misc.Unsafe UNSAFE;
|
66
|
+
static final long referenceOffset;
|
67
|
+
|
68
|
+
static {
|
69
|
+
try {
|
70
|
+
UNSAFE = UnsafeGetter.getUnsafe();
|
71
|
+
Class k = JRubyReference.class;
|
72
|
+
referenceOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("reference"));
|
73
|
+
} catch (Exception e) {
|
74
|
+
throw new RuntimeException(e);
|
75
|
+
}
|
76
|
+
}
|
54
77
|
|
55
78
|
public JRubyReference(Ruby runtime, RubyClass klass) {
|
56
79
|
super(runtime, klass);
|
57
|
-
reference = runtime.getNil();
|
58
80
|
}
|
59
81
|
|
60
82
|
@JRubyMethod
|
61
83
|
public IRubyObject initialize(ThreadContext context) {
|
62
|
-
|
63
|
-
|
64
|
-
return runtime.getNil();
|
84
|
+
UNSAFE.putObject(this, referenceOffset, context.nil);
|
85
|
+
return context.nil;
|
65
86
|
}
|
66
87
|
|
67
88
|
@JRubyMethod
|
68
89
|
public IRubyObject initialize(ThreadContext context, IRubyObject value) {
|
69
|
-
|
70
|
-
|
71
|
-
return runtime.getNil();
|
90
|
+
UNSAFE.putObject(this, referenceOffset, value);
|
91
|
+
return context.nil;
|
72
92
|
}
|
73
93
|
|
74
94
|
@JRubyMethod(name = {"get", "value"})
|
75
95
|
public IRubyObject get() {
|
76
|
-
return
|
96
|
+
return reference;
|
77
97
|
}
|
78
98
|
|
79
99
|
@JRubyMethod(name = {"set", "value="})
|
80
100
|
public IRubyObject set(IRubyObject newValue) {
|
81
|
-
|
101
|
+
UNSAFE.putObjectVolatile(this, referenceOffset, newValue);
|
82
102
|
return newValue;
|
83
103
|
}
|
84
104
|
|
85
|
-
@JRubyMethod
|
105
|
+
@JRubyMethod(name = {"compare_and_set", "compare_and_swap"})
|
86
106
|
public IRubyObject compare_and_set(ThreadContext context, IRubyObject oldValue, IRubyObject newValue) {
|
87
|
-
return context.
|
107
|
+
return context.runtime.newBoolean(UNSAFE.compareAndSwapObject(this, referenceOffset, oldValue, newValue));
|
88
108
|
}
|
89
109
|
|
90
|
-
@JRubyMethod
|
110
|
+
@JRubyMethod(name = {"get_and_set", "swap"})
|
111
|
+
public IRubyObject get_and_set(ThreadContext context, IRubyObject newValue) {
|
112
|
+
// less-efficient version for Java 6 and 7
|
113
|
+
while (true) {
|
114
|
+
IRubyObject oldValue = get();
|
115
|
+
if (UNSAFE.compareAndSwapObject(this, referenceOffset, oldValue, newValue)) {
|
116
|
+
return oldValue;
|
117
|
+
}
|
118
|
+
}
|
119
|
+
}
|
120
|
+
}
|
121
|
+
|
122
|
+
public static class JRubyReference8 extends JRubyReference {
|
123
|
+
public JRubyReference8(Ruby runtime, RubyClass klass) {
|
124
|
+
super(runtime, klass);
|
125
|
+
}
|
126
|
+
|
127
|
+
@Override
|
91
128
|
public IRubyObject get_and_set(ThreadContext context, IRubyObject newValue) {
|
92
|
-
|
129
|
+
// efficient version for Java 8
|
130
|
+
return (IRubyObject)UNSAFE.getAndSetObject(this, referenceOffset, newValue);
|
93
131
|
}
|
94
132
|
}
|
95
133
|
}
|
data/lib/atomic.rb
CHANGED
@@ -10,106 +10,9 @@
|
|
10
10
|
# See the License for the specific language governing permissions and
|
11
11
|
# limitations under the License.
|
12
12
|
|
13
|
-
require 'thread'
|
14
|
-
|
15
|
-
class Atomic
|
16
|
-
class ConcurrentUpdateError < ThreadError
|
17
|
-
end
|
18
|
-
|
19
|
-
def initialize(value=nil)
|
20
|
-
@ref = InternalReference.new(value)
|
21
|
-
end
|
22
|
-
|
23
|
-
def value
|
24
|
-
@ref.get
|
25
|
-
end
|
26
|
-
alias get value
|
27
|
-
|
28
|
-
def value=(new_value)
|
29
|
-
@ref.set(new_value)
|
30
|
-
new_value
|
31
|
-
end
|
32
|
-
alias set value=
|
33
|
-
|
34
|
-
def swap(new_value)
|
35
|
-
@ref.get_and_set(new_value)
|
36
|
-
end
|
37
|
-
alias get_and_set swap
|
38
|
-
|
39
|
-
def compare_and_swap(old_value, new_value)
|
40
|
-
@ref.compare_and_set(old_value, new_value)
|
41
|
-
end
|
42
|
-
alias compare_and_set compare_and_swap
|
43
|
-
|
44
|
-
# Pass the current value to the given block, replacing it
|
45
|
-
# with the block's result. May retry if the value changes
|
46
|
-
# during the block's execution.
|
47
|
-
def update
|
48
|
-
true until @ref.compare_and_set(old_value = @ref.get, new_value = yield(old_value))
|
49
|
-
new_value
|
50
|
-
end
|
51
|
-
|
52
|
-
# frozen pre-allocated backtrace to speed ConcurrentUpdateError
|
53
|
-
CONC_UP_ERR_BACKTRACE = ['backtrace elided; set verbose to enable'].freeze
|
54
|
-
|
55
|
-
def try_update
|
56
|
-
old_value = @ref.get
|
57
|
-
new_value = yield old_value
|
58
|
-
unless @ref.compare_and_set(old_value, new_value)
|
59
|
-
if $VERBOSE
|
60
|
-
raise ConcurrentUpdateError, "Update failed"
|
61
|
-
else
|
62
|
-
raise ConcurrentUpdateError, "Update failed", CONC_UP_ERR_BACKTRACE
|
63
|
-
end
|
64
|
-
end
|
65
|
-
new_value
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
13
|
begin
|
70
14
|
ruby_engine = defined?(RUBY_ENGINE)? RUBY_ENGINE : 'ruby'
|
71
|
-
|
72
|
-
when 'jruby', 'ruby'
|
73
|
-
require 'atomic_reference'
|
74
|
-
when 'rbx'
|
75
|
-
Atomic::InternalReference = Rubinius::AtomicReference
|
76
|
-
else
|
77
|
-
raise LoadError
|
78
|
-
end
|
15
|
+
require "atomic/#{ruby_engine}"
|
79
16
|
rescue LoadError
|
80
|
-
|
81
|
-
|
82
|
-
class Atomic::InternalReference #:nodoc: all
|
83
|
-
def initialize(value)
|
84
|
-
@mutex = Mutex.new
|
85
|
-
@value = value
|
86
|
-
end
|
87
|
-
|
88
|
-
def get
|
89
|
-
@mutex.synchronize { @value }
|
90
|
-
end
|
91
|
-
|
92
|
-
def set(new_value)
|
93
|
-
@mutex.synchronize { @value = new_value }
|
94
|
-
end
|
95
|
-
|
96
|
-
def get_and_set(new_value)
|
97
|
-
@mutex.synchronize do
|
98
|
-
old_value = @value
|
99
|
-
@value = new_value
|
100
|
-
old_value
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
def compare_and_set(old_value, new_value)
|
105
|
-
return false unless @mutex.try_lock
|
106
|
-
begin
|
107
|
-
return false unless @value.equal? old_value
|
108
|
-
@value = new_value
|
109
|
-
ensure
|
110
|
-
@mutex.unlock
|
111
|
-
end
|
112
|
-
true
|
113
|
-
end
|
114
|
-
end
|
115
|
-
end
|
17
|
+
require 'atomic/fallback'
|
18
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'atomic/concurrent_update_error'
|
2
|
+
|
3
|
+
# Define update methods that delegate to @ref field
|
4
|
+
class Atomic
|
5
|
+
# Pass the current value to the given block, replacing it
|
6
|
+
# with the block's result. May retry if the value changes
|
7
|
+
# during the block's execution.
|
8
|
+
def update
|
9
|
+
true until @ref.compare_and_set(old_value = @ref.get, new_value = yield(old_value))
|
10
|
+
new_value
|
11
|
+
end
|
12
|
+
|
13
|
+
def try_update
|
14
|
+
old_value = @ref.get
|
15
|
+
new_value = yield old_value
|
16
|
+
unless @ref.compare_and_set(old_value, new_value)
|
17
|
+
if $VERBOSE
|
18
|
+
raise ConcurrentUpdateError, "Update failed"
|
19
|
+
else
|
20
|
+
raise ConcurrentUpdateError, "Update failed", ConcurrentUpdateError::CONC_UP_ERR_BACKTRACE
|
21
|
+
end
|
22
|
+
end
|
23
|
+
new_value
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'atomic/concurrent_update_error'
|
2
|
+
|
3
|
+
# Define update methods that use direct paths
|
4
|
+
class Atomic
|
5
|
+
# Pass the current value to the given block, replacing it
|
6
|
+
# with the block's result. May retry if the value changes
|
7
|
+
# during the block's execution.
|
8
|
+
def update
|
9
|
+
true until compare_and_set(old_value = get, new_value = yield(old_value))
|
10
|
+
new_value
|
11
|
+
end
|
12
|
+
|
13
|
+
def try_update
|
14
|
+
old_value = get
|
15
|
+
new_value = yield old_value
|
16
|
+
unless compare_and_set(old_value, new_value)
|
17
|
+
if $VERBOSE
|
18
|
+
raise ConcurrentUpdateError, "Update failed"
|
19
|
+
else
|
20
|
+
raise ConcurrentUpdateError, "Update failed", ConcurrentUpdateError::CONC_UP_ERR_BACKTRACE
|
21
|
+
end
|
22
|
+
end
|
23
|
+
new_value
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
warn "#{__FILE__}:#{__LINE__}: unsupported Ruby engine `#{RUBY_ENGINE}', using less-efficient Atomic impl"
|
2
|
+
|
3
|
+
require 'thread'
|
4
|
+
require 'atomic/direct_update'
|
5
|
+
|
6
|
+
# Portable/generic (but not very memory or scheduling-efficient) fallback
|
7
|
+
class Atomic #:nodoc: all
|
8
|
+
def initialize(value = nil)
|
9
|
+
@mutex = Mutex.new
|
10
|
+
@value = value
|
11
|
+
end
|
12
|
+
|
13
|
+
def get
|
14
|
+
@mutex.synchronize { @value }
|
15
|
+
end
|
16
|
+
alias value get
|
17
|
+
|
18
|
+
def set(new_value)
|
19
|
+
@mutex.synchronize { @value = new_value }
|
20
|
+
end
|
21
|
+
alias value= set
|
22
|
+
|
23
|
+
def get_and_set(new_value)
|
24
|
+
@mutex.synchronize do
|
25
|
+
old_value = @value
|
26
|
+
@value = new_value
|
27
|
+
old_value
|
28
|
+
end
|
29
|
+
end
|
30
|
+
alias swap get_and_set
|
31
|
+
|
32
|
+
def compare_and_set(old_value, new_value)
|
33
|
+
return false unless @mutex.try_lock
|
34
|
+
begin
|
35
|
+
return false unless @value.equal? old_value
|
36
|
+
@value = new_value
|
37
|
+
ensure
|
38
|
+
@mutex.unlock
|
39
|
+
end
|
40
|
+
true
|
41
|
+
end
|
42
|
+
alias compare_and_swap compare_and_set
|
43
|
+
end
|
data/lib/atomic/jruby.rb
ADDED
data/lib/atomic/rbx.rb
ADDED
data/lib/atomic/ruby.rb
ADDED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: atomic
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2013-
|
13
|
+
date: 2013-04-04 00:00:00.000000000 Z
|
14
14
|
dependencies: []
|
15
15
|
description: An atomic reference implementation for JRuby, Rubinius, and MRI
|
16
16
|
email:
|
@@ -24,6 +24,7 @@ files:
|
|
24
24
|
- test/test_atomic.rb
|
25
25
|
- ext/extconf.rb
|
26
26
|
- .gitignore
|
27
|
+
- .travis.yml
|
27
28
|
- LICENSE
|
28
29
|
- README.md
|
29
30
|
- Rakefile
|
@@ -36,6 +37,13 @@ files:
|
|
36
37
|
- ext/atomic_reference.c
|
37
38
|
- ext/org/jruby/ext/atomic/AtomicReferenceLibrary.java
|
38
39
|
- lib/atomic.rb
|
40
|
+
- lib/atomic/concurrent_update_error.rb
|
41
|
+
- lib/atomic/delegated_update.rb
|
42
|
+
- lib/atomic/direct_update.rb
|
43
|
+
- lib/atomic/fallback.rb
|
44
|
+
- lib/atomic/jruby.rb
|
45
|
+
- lib/atomic/rbx.rb
|
46
|
+
- lib/atomic/ruby.rb
|
39
47
|
homepage: http://github.com/headius/ruby-atomic
|
40
48
|
licenses: []
|
41
49
|
post_install_message:
|