h8 0.0.2 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|