h8 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +8 -0
- data/README.md +55 -2
- data/ext/h8/JsCatcher.cpp +24 -0
- data/ext/h8/JsCatcher.h +29 -0
- data/ext/h8/allocated_resource.h +37 -0
- data/ext/h8/chain.h +191 -0
- data/ext/h8/extconf.rb +48 -30
- data/ext/h8/h8.cpp +100 -8
- data/ext/h8/h8.h +109 -42
- data/ext/h8/js_gate.cpp +18 -0
- data/ext/h8/js_gate.h +70 -31
- data/ext/h8/main.cpp +69 -25
- data/ext/h8/object_wrap.h +1 -1
- data/ext/h8/ruby_gate.cpp +117 -0
- data/ext/h8/ruby_gate.h +118 -0
- data/hybrid8.gemspec +3 -1
- data/lib/h8.rb +61 -2
- data/lib/h8/context.rb +38 -5
- data/lib/h8/value.rb +104 -19
- data/lib/h8/version.rb +1 -1
- data/spec/context_spec.rb +42 -4
- data/spec/js_gate_spec.rb +95 -16
- data/spec/ruby_gate_spec.rb +159 -0
- data/spec/spec_helper.rb +31 -1
- metadata +15 -4
- data/ext/h8/ruby_wrap.h +0 -32
data/lib/h8/context.rb
CHANGED
@@ -1,21 +1,54 @@
|
|
1
1
|
module H8
|
2
2
|
class Context
|
3
|
+
# Create new context optionally providing variables hash
|
3
4
|
def initialize timout: nil, **kwargs
|
4
|
-
|
5
|
+
set_all **kwargs
|
5
6
|
end
|
6
7
|
|
7
|
-
|
8
|
+
# set variables from keyword arguments to this context
|
9
|
+
def set_all **kwargs
|
8
10
|
kwargs.each { |name, value|
|
9
11
|
set_var(name.to_s, value)
|
10
12
|
}
|
11
13
|
end
|
12
14
|
|
15
|
+
# Set variable for the context
|
13
16
|
def []= name, value
|
14
|
-
|
17
|
+
set_all name => value
|
15
18
|
end
|
16
19
|
|
17
|
-
|
18
|
-
|
20
|
+
# Execute a given script on the current context with optionally limited execution time.
|
21
|
+
#
|
22
|
+
# @param [Float] timeout if is not 0 then maximum execution time in seconds
|
23
|
+
# @return [Value] wrapped object returned by the script
|
24
|
+
# @raise [H8::TimeoutError] if the timeout was set and expired
|
25
|
+
def eval script, timeout: 0
|
26
|
+
_eval script, (timeout*1000).to_i
|
27
|
+
end
|
28
|
+
|
29
|
+
# Execute script in a new context with optionally set vars. @see H8#set_all
|
30
|
+
# @return [Value] wrapped object returned by the script
|
31
|
+
def self.eval script, **kwargs
|
32
|
+
Context.new(** kwargs).eval script
|
33
|
+
end
|
34
|
+
|
35
|
+
# Secure gate for JS to securely access ruby class properties (methods with no args)
|
36
|
+
# and methods.
|
37
|
+
def self.secure_call instance, method, args=nil
|
38
|
+
method = method.to_sym
|
39
|
+
begin
|
40
|
+
m = instance.public_method(method)
|
41
|
+
if m.owner == instance.class
|
42
|
+
return m.call(*args) if method[-1] == '='
|
43
|
+
if m.arity != 0
|
44
|
+
return -> (*args) { m.call *args }
|
45
|
+
else
|
46
|
+
return m.call *args
|
47
|
+
end
|
48
|
+
end
|
49
|
+
rescue NameError
|
50
|
+
end
|
51
|
+
H8::Undefined
|
19
52
|
end
|
20
53
|
end
|
21
54
|
end
|
data/lib/h8/value.rb
CHANGED
@@ -2,62 +2,108 @@ module H8
|
|
2
2
|
|
3
3
|
# Wrapper for javascript objects.
|
4
4
|
#
|
5
|
-
# Important: when
|
6
|
-
# js notation, instead, check the returned value to be
|
5
|
+
# Important: when accessing fields of the object, respond_to? will not work due to
|
6
|
+
# js notation, instead, check the returned value (there will be always one) to not to be
|
7
|
+
# value.undefined?
|
8
|
+
#
|
9
|
+
# Precaution! Values are always bounded to the context where they are created. You can not
|
10
|
+
# pass values between context, as they often map native Javascript objects. If you need, you
|
11
|
+
# can copy, for example, by converting it H8::Value#to_ruby first. Another approach is to
|
12
|
+
# use JSON serialization from the script.
|
7
13
|
class Value
|
8
14
|
|
9
15
|
include Comparable
|
10
16
|
|
11
17
|
def inspect
|
12
|
-
"<H8::Value #{
|
18
|
+
"<H8::Value #{to_ruby}>"
|
13
19
|
end
|
14
20
|
|
15
|
-
# Get js object attribute by
|
21
|
+
# Get js object attribute by either name or index (should be Fixnum instance). It always
|
16
22
|
# return H8::Value instance, check it to (not) be undefined? to see if there is such attribute
|
23
|
+
#
|
24
|
+
# @return [H8::Value] instance, which is .undefined? if does not exist
|
17
25
|
def [] name_index
|
18
26
|
name_index.is_a?(Fixnum) ? _get_index(name_index) : _get_attr(name_index)
|
19
27
|
end
|
20
28
|
|
21
|
-
# Optimized JS member access.
|
22
|
-
#
|
29
|
+
# Optimized JS member access. Support class members and member functions - will call them
|
30
|
+
# automatically. Use val['attr'] to get callable instead
|
23
31
|
def method_missing(method_sym, *arguments, &block)
|
24
32
|
name = method_sym.to_s
|
25
33
|
instance_eval <<-End
|
26
34
|
def #{name} *args, **kwargs
|
27
35
|
res = _get_attr('#{name}')
|
28
|
-
res.function? ? res.apply(
|
36
|
+
(res.is_a?(H8::Value) && res.function?) ? res.apply(self,*args) : res
|
29
37
|
end
|
30
38
|
End
|
31
39
|
send method_sym, *arguments
|
32
40
|
end
|
33
41
|
|
34
|
-
#
|
35
|
-
# p eval("Object.keys(this)");
|
36
|
-
# end
|
37
|
-
#
|
38
|
-
# def keys
|
39
|
-
# cxt = self._context
|
40
|
-
# cxt['__self'] =
|
41
|
-
# end
|
42
|
-
|
42
|
+
# Compare to other object, either usual or another Value instance. Does its best.
|
43
43
|
def <=> other
|
44
44
|
other = other.to_ruby if other.is_a?(H8::Value)
|
45
45
|
to_ruby <=> other
|
46
46
|
end
|
47
47
|
|
48
|
+
# Call javascript function represented by this instance (which should be function())
|
49
|
+
# with given (or no) arguments.
|
50
|
+
# @return [H8::Value] function return which might be undefined
|
48
51
|
def call *args
|
49
52
|
_call args
|
50
53
|
end
|
51
54
|
|
52
|
-
|
53
|
-
|
55
|
+
# Like JS apply: call the value that should be function() bounded to a given object
|
56
|
+
# @param [Object] this object to bound call to
|
57
|
+
# @param args any arguments
|
58
|
+
# @return [H8::Value] result returned by the function which might be undefined
|
59
|
+
def apply this, *args
|
60
|
+
_apply this, args
|
54
61
|
end
|
55
62
|
|
63
|
+
# Tries to convert JS object to ruby array
|
64
|
+
# @raise H8::Error if the JS object is not an array
|
56
65
|
def to_ary
|
57
66
|
raise Error, 'Is not an array' unless array?
|
58
67
|
to_ruby
|
59
68
|
end
|
60
69
|
|
70
|
+
alias :to_a :to_ary
|
71
|
+
|
72
|
+
# Generate set of keys of the wrapped object
|
73
|
+
def keys
|
74
|
+
context[:__obj] = self
|
75
|
+
Set.new context.eval("(Object.keys(__obj));").to_a
|
76
|
+
end
|
77
|
+
|
78
|
+
# enumerate |key, value| pairs for javascript object attributes
|
79
|
+
def each
|
80
|
+
return enum_for(:each) unless block_given? # Sparkling magic!
|
81
|
+
keys.each { |k|
|
82
|
+
yield k, _get_attr(k)
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
# Try to convert javascript object to a ruby hash
|
87
|
+
def to_h
|
88
|
+
each.reduce({}) { |all, kv| all[kv[0]] = kv[1].to_ruby; all }
|
89
|
+
end
|
90
|
+
|
91
|
+
# Iterate over javascript object keys
|
92
|
+
def each_key
|
93
|
+
keys.each
|
94
|
+
end
|
95
|
+
|
96
|
+
# @return [Array] values array. does NOT convert values to_ruby()
|
97
|
+
def values
|
98
|
+
each.reduce([]) { |all, kv| all << kv[1]; all }
|
99
|
+
end
|
100
|
+
|
101
|
+
# iterate over values of the javascript object attributes
|
102
|
+
def each_value
|
103
|
+
values.each
|
104
|
+
end
|
105
|
+
|
106
|
+
# Tries to convert wrapped JS object to ruby primitive (Fixed, String, Float, Array, Hash)
|
61
107
|
def to_ruby
|
62
108
|
case
|
63
109
|
when integer?
|
@@ -68,10 +114,49 @@ module H8
|
|
68
114
|
to_f
|
69
115
|
when array?
|
70
116
|
_get_attr('length').to_i.times.map { |i| _get_index(i).to_ruby }
|
117
|
+
when object?
|
118
|
+
to_h
|
71
119
|
else
|
72
|
-
raise Error, "Dont know how to convert
|
120
|
+
raise Error, "Dont know how to convert #{self.class}"
|
73
121
|
end
|
74
122
|
end
|
123
|
+
|
124
|
+
def function? # native method. stub for documentation
|
125
|
+
end
|
126
|
+
|
127
|
+
def object? # native method. stub for documentation
|
128
|
+
end
|
129
|
+
|
130
|
+
def undefined? # native method. stub for documentation
|
131
|
+
end
|
132
|
+
|
133
|
+
def array? # native method. stub for documentation
|
134
|
+
end
|
135
|
+
|
136
|
+
# @return [H8::Context] context to which this value is bounded
|
137
|
+
def context # native method. stub for documentation
|
138
|
+
end
|
139
|
+
|
140
|
+
# Convert to Proc instance so it could be used as &block parameter
|
141
|
+
# @raises (H8::Error) if !self.function?
|
142
|
+
def to_proc
|
143
|
+
function? or raise H8::Error, 'JS object is not a function'
|
144
|
+
-> (*args) { call *args }
|
145
|
+
end
|
146
|
+
|
147
|
+
def to_str
|
148
|
+
to_s
|
149
|
+
end
|
75
150
|
end
|
76
151
|
|
77
152
|
end
|
153
|
+
|
154
|
+
# Ruby object's t_ruby does nothing (tree conversion optimization)
|
155
|
+
class Object
|
156
|
+
# It is already a ruby object. Gate objects should override
|
157
|
+
# as need
|
158
|
+
def to_ruby
|
159
|
+
self
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
data/lib/h8/version.rb
CHANGED
data/spec/context_spec.rb
CHANGED
@@ -5,7 +5,9 @@ describe 'context' do
|
|
5
5
|
|
6
6
|
it 'should create' do
|
7
7
|
cxt = H8::Context.new
|
8
|
-
cxt
|
8
|
+
cxt[:one] = 1
|
9
|
+
cxt.eval("");
|
10
|
+
# cxt.eval("'Res: ' + (2+5);")
|
9
11
|
end
|
10
12
|
|
11
13
|
it 'should gate simple values to JS context' do
|
@@ -13,7 +15,7 @@ describe 'context' do
|
|
13
15
|
cxt[:sign] = '!'
|
14
16
|
res = cxt.eval "foo+' '+bar+sign;"
|
15
17
|
res.should == 'hello world!'
|
16
|
-
cxt.
|
18
|
+
cxt.set_all one: 101, real: 1.21
|
17
19
|
cxt.eval("one + one;").should == 202
|
18
20
|
cxt.eval("real + one;").should == (101 + 1.21)
|
19
21
|
end
|
@@ -28,12 +30,48 @@ describe 'context' do
|
|
28
30
|
|
29
31
|
it 'should not gate H8::Values between contexts' do
|
30
32
|
cxt = H8::Context.new
|
31
|
-
obj = cxt.eval "('che bel');"
|
33
|
+
obj = cxt.eval "({res: 'che bel'});"
|
34
|
+
# This should be ok
|
35
|
+
cxt[:first] = obj
|
36
|
+
res = cxt.eval "first.res + ' giorno';"
|
37
|
+
res.should == 'che bel giorno'
|
38
|
+
# And that should fail
|
32
39
|
cxt1 = H8::Context.new
|
33
40
|
expect( -> {
|
34
41
|
cxt1[:first] = obj
|
35
|
-
res = cxt1.eval "first + ' giorno';"
|
42
|
+
res = cxt1.eval "first.res + ' giorno';"
|
36
43
|
}).to raise_error(H8::Error)
|
37
44
|
end
|
38
45
|
|
46
|
+
it 'should provide reasonable undefined logic' do
|
47
|
+
raise "bad !undefined" if !!H8::Undefined
|
48
|
+
H8::Undefined.should_not == true
|
49
|
+
H8::Undefined.should == false
|
50
|
+
H8::Undefined.should_not == 11
|
51
|
+
(H8::Undefined==nil).should == false
|
52
|
+
H8::Undefined.should be_undefined
|
53
|
+
(!H8::Undefined).should == true
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'should limit script execution time' do
|
57
|
+
cxt = H8::Context.new
|
58
|
+
# cxt[:print] = -> (*args) { puts args.join(' ')}
|
59
|
+
script = <<-End
|
60
|
+
var start = new Date();
|
61
|
+
var last = null;
|
62
|
+
var counter = 0;
|
63
|
+
while((last=new Date().getTime()) - start < 1000 ) {
|
64
|
+
counter++;
|
65
|
+
}
|
66
|
+
counter;
|
67
|
+
End
|
68
|
+
# end
|
69
|
+
t = Time.now
|
70
|
+
expect( -> {
|
71
|
+
c2 = cxt.eval script, timeout: 0.2
|
72
|
+
}).to raise_error(H8::TimeoutError)
|
73
|
+
(Time.now - t).should < 0.25
|
74
|
+
cxt.eval('(last-start)/1000').should < 250
|
75
|
+
end
|
76
|
+
|
39
77
|
end
|
data/spec/js_gate_spec.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require 'h8'
|
3
|
+
require 'weakref'
|
3
4
|
|
4
5
|
describe 'js_gate' do
|
5
6
|
|
@@ -18,26 +19,25 @@ describe 'js_gate' do
|
|
18
19
|
res.should_not be_integer
|
19
20
|
res = cxt.eval("11+22;")
|
20
21
|
res.to_i.should == 33
|
21
|
-
res.should
|
22
|
-
res.should be_float
|
23
|
-
res.should_not be_string
|
22
|
+
res.should be_kind_of(Fixnum)
|
24
23
|
end
|
25
24
|
|
26
25
|
it 'should return floats' do
|
27
26
|
cxt = H8::Context.new
|
28
27
|
res = cxt.eval("0.2 + 122.1;")
|
29
28
|
res.to_s.should == '122.3'
|
30
|
-
res.should
|
31
|
-
res.should_not be_integer
|
32
|
-
res.should_not be_string
|
29
|
+
res.should be_kind_of(Float)
|
33
30
|
end
|
34
31
|
|
35
32
|
it 'should return strings' do
|
36
33
|
res = H8::Context.eval("'hel' + 'lo';")
|
37
34
|
res.to_s.should == 'hello'
|
38
|
-
res.should
|
39
|
-
|
40
|
-
|
35
|
+
res.should be_kind_of(String)
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should return undefined and null' do
|
39
|
+
H8::Context.eval('undefined').should == H8::Undefined
|
40
|
+
H8::Context.eval('null').should == nil
|
41
41
|
end
|
42
42
|
|
43
43
|
it 'should retreive JS fieds as indexes' do
|
@@ -46,7 +46,7 @@ describe 'js_gate' do
|
|
46
46
|
res['bar'].to_i.should == 122
|
47
47
|
end
|
48
48
|
|
49
|
-
it 'should retreive JS
|
49
|
+
it 'should retreive JS fields as properties' do
|
50
50
|
res = H8::Context.eval("({ 'foo': 'bar', 'bar': 122 });")
|
51
51
|
res.bar.to_i.should == 122
|
52
52
|
res.foo.to_s.should == 'bar'
|
@@ -55,6 +55,9 @@ describe 'js_gate' do
|
|
55
55
|
# cached method check
|
56
56
|
res.foo.to_s.should == 'bar'
|
57
57
|
res.bar.to_i.should == 122
|
58
|
+
|
59
|
+
res.bad.should == H8::Undefined
|
60
|
+
# res.bad.bad ?
|
58
61
|
end
|
59
62
|
|
60
63
|
it 'should access arrays' do
|
@@ -67,11 +70,16 @@ describe 'js_gate' do
|
|
67
70
|
res[1].to_s.should == 'foo'
|
68
71
|
res[2].to_s.should == 'bar'
|
69
72
|
}
|
73
|
+
res[3].should == H8::Undefined
|
70
74
|
end
|
71
75
|
|
72
76
|
it 'should eval and keep context alive' do
|
73
|
-
|
74
|
-
|
77
|
+
cxt = H8::Context.new
|
78
|
+
wr = WeakRef.new cxt
|
79
|
+
obj = cxt.eval("({ 'foo': 'bar', 'bar': 122 });")
|
80
|
+
cxt = nil
|
81
|
+
GC.start # cxt is now kept only by H8::Value obj
|
82
|
+
wr.weakref_alive?.should be_true
|
75
83
|
obj.foo.should == 'bar'
|
76
84
|
end
|
77
85
|
|
@@ -94,13 +102,21 @@ describe 'js_gate' do
|
|
94
102
|
res = H8::Context.eval("[-10, 'foo', 'bar'];")
|
95
103
|
res.to_ruby.should == [-10, 'foo', 'bar']
|
96
104
|
res.to_ary.should == [-10, 'foo', 'bar']
|
97
|
-
expect(-> { H8::Context.eval("'not array'").to_ary}).to raise_error(H8::Error)
|
98
105
|
end
|
99
106
|
|
100
107
|
it 'should provide hash methods' do
|
101
|
-
pending
|
102
108
|
obj = H8::Context.eval("({ 'foo': 'bar', 'bar': 122 });")
|
103
|
-
obj.keys.should == ['foo', 'bar']
|
109
|
+
obj.keys.should == Set.new(['foo', 'bar'])
|
110
|
+
|
111
|
+
hash = {}
|
112
|
+
obj.each { |k, v| hash[k] = v.to_ruby }
|
113
|
+
hash.should == { "foo" => "bar", "bar" => 122 }
|
114
|
+
obj.to_h.should == { "foo" => "bar", "bar" => 122 }
|
115
|
+
obj.to_ruby.should == { "foo" => "bar", "bar" => 122 }
|
116
|
+
|
117
|
+
Set.new(obj.each_key).should == Set.new(['foo', 'bar'])
|
118
|
+
Set.new(obj.values.map(&:to_ruby)).should == Set.new(['bar', 122])
|
119
|
+
Set.new(obj.each_value.map(&:to_ruby)).should == Set.new(['bar', 122])
|
104
120
|
end
|
105
121
|
|
106
122
|
it 'should convert compare to ruby objects' do
|
@@ -137,7 +153,7 @@ describe 'js_gate' do
|
|
137
153
|
end
|
138
154
|
|
139
155
|
it 'should raise error on syntax' do
|
140
|
-
expect(
|
156
|
+
expect(-> {
|
141
157
|
H8::Context.eval 'this is not a valid js'
|
142
158
|
}).to raise_error(H8::Error)
|
143
159
|
end
|
@@ -159,4 +175,67 @@ describe 'js_gate' do
|
|
159
175
|
res.doAdd(10, 1).should == 111
|
160
176
|
end
|
161
177
|
|
178
|
+
it 'should pass exceptions from member function calls' do
|
179
|
+
res = H8::Context.eval <<-End
|
180
|
+
function cls(base) {
|
181
|
+
this.base = base;
|
182
|
+
this.doAdd = function(a, b) {
|
183
|
+
throw Error("Test error")
|
184
|
+
return a + b + base;
|
185
|
+
}
|
186
|
+
}
|
187
|
+
new cls(100);
|
188
|
+
End
|
189
|
+
expect(-> {
|
190
|
+
res.doAdd(10, 1).should == 111
|
191
|
+
}).to raise_error(H8::JsError) { |e|
|
192
|
+
e.message.should == "Uncaught Error: Test error"
|
193
|
+
}
|
194
|
+
end
|
195
|
+
|
196
|
+
it 'should call js functions' do
|
197
|
+
res = H8::Context.eval <<-End
|
198
|
+
var fn = function cls(a, b) {
|
199
|
+
return a + ":" + b;
|
200
|
+
}
|
201
|
+
fn;
|
202
|
+
End
|
203
|
+
res.call('foo', 'bar').should == 'foo:bar'
|
204
|
+
|
205
|
+
def xx(val, &block)
|
206
|
+
"::" + val + "-" + block.call('hello', 'world')
|
207
|
+
end
|
208
|
+
|
209
|
+
xx("123", &res.to_proc).should == "::123-hello:world"
|
210
|
+
end
|
211
|
+
|
212
|
+
it 'should gate uncaught exceptions from js callbacks' do
|
213
|
+
res = H8::Context.eval <<-End
|
214
|
+
var fn = function cls(a, b) {
|
215
|
+
throw Error("the test error");
|
216
|
+
}
|
217
|
+
fn;
|
218
|
+
End
|
219
|
+
expect(-> {
|
220
|
+
res.call('foo', 'bar').should == 'foo:bar'
|
221
|
+
}).to raise_error(H8::JsError)
|
222
|
+
end
|
223
|
+
|
224
|
+
it 'should pass thru uncaught ruby exceptions from js->ruby callbacks' do
|
225
|
+
class MyException < StandardError;
|
226
|
+
end;
|
227
|
+
cxt = H8::Context.new
|
228
|
+
cxt[:bad_call] = -> { raise MyException }
|
229
|
+
res = cxt.eval <<-End
|
230
|
+
var fn = function cls(a, b) {
|
231
|
+
bad_call();
|
232
|
+
}
|
233
|
+
fn;
|
234
|
+
End
|
235
|
+
expect(-> {
|
236
|
+
res.call('foo', 'bar').should == 'foo:bar'
|
237
|
+
}).to raise_error(MyException)
|
238
|
+
end
|
239
|
+
|
240
|
+
|
162
241
|
end
|