h8 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +2 -0
- data/README.md +68 -4
- data/ext/h8/extconf.rb +29 -10
- data/ext/h8/h8.cpp +16 -0
- data/ext/h8/h8.h +159 -0
- data/ext/h8/js_gate.h +196 -0
- data/ext/h8/main.cpp +162 -0
- data/ext/h8/object_wrap.h +138 -0
- data/ext/h8/ruby_wrap.h +32 -0
- data/hybrid8.gemspec +23 -4
- data/lib/h8.rb +9 -0
- data/lib/h8/context.rb +21 -0
- data/lib/h8/value.rb +77 -0
- data/lib/h8/version.rb +1 -1
- data/spec/context_spec.rb +39 -0
- data/spec/js_gate_spec.rb +162 -0
- data/spec/spec_helper.rb +17 -0
- metadata +39 -8
- data/lib/hybrid8.rb +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7353dcd3c307b93e4f7fd68ccbc7af260e51d09c
|
4
|
+
data.tar.gz: 8603a9cfb342305c5dfcf156bf92d595bde23a34
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 94056bd7b5378d50ceefd3322479e38b3eb122ee019a0c4ce4a9531bb447fa062e26393c4a5738d97138eaf973a6627b55002126850133fe59c6a9b652028c96
|
7
|
+
data.tar.gz: b1a98f23583bc8cb77ffd35bae2cb1f3d71ea6b9e6f6553149ee2162b468ff9474654d6069d5aa68bb8c0cd97713b26caa24b08604f4b52bbd8b8cca92cc5eb5
|
data/.rspec
ADDED
data/README.md
CHANGED
@@ -1,11 +1,30 @@
|
|
1
|
-
# Hybrid8
|
1
|
+
# Hybrid8, aka H8
|
2
2
|
|
3
|
-
_Warning_ this gem
|
3
|
+
_Warning_ this gem has yet limited functionality and is under main development. Versions suitable
|
4
|
+
for an open beta test will start from 0.1.*.
|
4
5
|
|
5
|
-
Therubyracer gem alternative to work with ruby 2.1+ in multithreaded environment
|
6
|
+
Therubyracer gem alternative to effectively work with ruby 2.1+ in multithreaded environment in an
|
7
|
+
effective and GC-safe way. Should be out of the box replacement for most scenarios.
|
8
|
+
|
9
|
+
Special features:
|
10
|
+
|
11
|
+
- care about wrapped Ruby objects lifespan (are kept until either ruby or javascript context
|
12
|
+
reference wrapped object). In no way GC will not reclaim ruby object that is in use by the
|
13
|
+
javascript context
|
14
|
+
|
15
|
+
- care about wrapped Javascript objects lifetime the same. Referenced JS items will not be recycled
|
16
|
+
while there are ruby objects referencing it. It also means that once created H8::Context will not
|
17
|
+
be reclaimed as long as there is at least one active wrapped object returned from the script.
|
6
18
|
|
7
19
|
## Installation
|
8
20
|
|
21
|
+
### Prerequisites
|
22
|
+
|
23
|
+
You should have installed libv8, use latest version with v8::Isolate and v8::Locker. This version
|
24
|
+
may not find you installation, contact me if you have problems, I'll tune it up.
|
25
|
+
|
26
|
+
### Setting up
|
27
|
+
|
9
28
|
Add this line to your application's Gemfile:
|
10
29
|
|
11
30
|
gem 'hybrid8'
|
@@ -20,10 +39,55 @@ Or install it yourself as:
|
|
20
39
|
|
21
40
|
## Usage
|
22
41
|
|
23
|
-
|
42
|
+
Is generally like therubyracer gem. Create context, set variables, run scripts.
|
43
|
+
|
44
|
+
require 'h8'
|
45
|
+
|
46
|
+
res = H8::Context.eval "({foo: 'hello', bar: 'world'});"
|
47
|
+
puts "#{res.foo} #{res.bar}"
|
48
|
+
|
49
|
+
another way to access attributes:
|
50
|
+
|
51
|
+
puts res['foo']
|
52
|
+
|
53
|
+
The same works with arrays:
|
54
|
+
|
55
|
+
res = H8::Context.eval "['foo', bar'];"
|
56
|
+
puts res[1]
|
57
|
+
|
58
|
+
To set context variables:
|
59
|
+
|
60
|
+
cxt = H8::Context.new some: 'value'
|
61
|
+
cxt[:pi] = 3.1415
|
62
|
+
|
63
|
+
You can return function and call it from ruby:
|
64
|
+
|
65
|
+
fun = cxt.eval "(function pi_n(n) { return pi * n; })"
|
66
|
+
p fun(2)
|
67
|
+
|
68
|
+
The same you can return objects and call its member functions - if a member is a function,
|
69
|
+
it will be called with given arguments:
|
70
|
+
|
71
|
+
res = H8::Context.eval <<-End
|
72
|
+
function cls(base) {
|
73
|
+
this.base = base;
|
74
|
+
this.someVal = 'hello!';
|
75
|
+
this.noArgs = function() { return 'world!'};
|
76
|
+
this.doAdd = function(a, b) {
|
77
|
+
return a + b + base;
|
78
|
+
}
|
79
|
+
}
|
80
|
+
new cls(100);
|
81
|
+
End
|
82
|
+
res.someVal.should == 'hello!'
|
83
|
+
res.noArgs.should == 'world!'
|
84
|
+
res.doAdd(10, 1).should == 111
|
24
85
|
|
25
86
|
## Contributing
|
26
87
|
|
88
|
+
Note that at this early point of development you better first talk to me to not to reinvent the
|
89
|
+
wheel.
|
90
|
+
|
27
91
|
1. Fork it ( https://github.com/[my-github-username]/hybrid8/fork )
|
28
92
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
29
93
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
data/ext/h8/extconf.rb
CHANGED
@@ -21,21 +21,23 @@ abort 'missing malloc()' unless have_func 'malloc'
|
|
21
21
|
abort 'missing free()' unless have_func 'free'
|
22
22
|
|
23
23
|
# Give it a name
|
24
|
-
extension_name = '
|
24
|
+
extension_name = 'h8'
|
25
25
|
|
26
|
+
dir_config('v8', '/Users/sergeych/dev/v8', '/Users/sergeych/dev/v8/lib')
|
26
27
|
|
27
28
|
dir_config(extension_name)
|
28
29
|
ok = true
|
29
30
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
31
|
+
unless have_header('include/v8.h')
|
32
|
+
$stderr.puts "can't find v8.h, install libv8 3.25.30+ first"
|
33
|
+
ok = false
|
34
|
+
end
|
35
|
+
|
36
|
+
unless have_library('v8_base') && have_library('v8_snapshot') && have_library('v8_libplatform') \
|
37
|
+
&& have_library('v8_libbase') && have_library('icuuc') && have_library('icudata')
|
38
|
+
$stderr.puts "can't find libv8"
|
39
|
+
ok = false
|
40
|
+
end
|
39
41
|
|
40
42
|
|
41
43
|
# This test is actually due to a Clang 3.3 shortcoming, included in OS X 10.9,
|
@@ -60,3 +62,20 @@ else
|
|
60
62
|
raise "Unable to build, correct above errors and rerun"
|
61
63
|
end
|
62
64
|
|
65
|
+
# LIBV8_COMPATIBILITY = '~> 3.30'
|
66
|
+
#
|
67
|
+
# begin
|
68
|
+
# require 'rubygems'
|
69
|
+
# gem 'libv8', LIBV8_COMPATIBILITY
|
70
|
+
# rescue Gem::LoadError
|
71
|
+
# warn "Warning! Unable to load libv8 #{LIBV8_COMPATIBILITY}."
|
72
|
+
# rescue LoadError
|
73
|
+
# warn "Warning! Could not load rubygems. Please make sure you have libv8 #{LIBV8_COMPATIBILITY} installed."
|
74
|
+
# ensure
|
75
|
+
# require 'libv8'
|
76
|
+
# end
|
77
|
+
#
|
78
|
+
# Libv8.configure_makefile
|
79
|
+
|
80
|
+
create_makefile('h8/h8')
|
81
|
+
|
data/ext/h8/h8.cpp
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#include "h8.h"
|
2
|
+
|
3
|
+
Local<Value> h8::H8::gateObject(VALUE ruby_value) const {
|
4
|
+
if ( Qtrue == rb_funcall(ruby_value, id_is_a, 1, value_class)) {
|
5
|
+
JsGate *gate;
|
6
|
+
Data_Get_Struct(ruby_value, JsGate, gate);
|
7
|
+
if( gate->h8 != this ) {
|
8
|
+
rb_raise(h8_exception, "H8::Value is bound to other H8::Context");
|
9
|
+
return Undefined(isolate);
|
10
|
+
}
|
11
|
+
else
|
12
|
+
return gate->value();
|
13
|
+
}
|
14
|
+
rb_raise(h8_exception, "Object gate is not implemented");
|
15
|
+
return Undefined(isolate);
|
16
|
+
}
|
data/ext/h8/h8.h
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
#ifndef _h8_h
|
2
|
+
#define _h8_h
|
3
|
+
|
4
|
+
#include <include/v8.h>
|
5
|
+
#include <ruby.h>
|
6
|
+
#include <iostream>
|
7
|
+
|
8
|
+
using namespace v8;
|
9
|
+
using namespace std;
|
10
|
+
|
11
|
+
extern VALUE h8_exception;
|
12
|
+
extern VALUE h8_class;
|
13
|
+
extern VALUE value_class;
|
14
|
+
|
15
|
+
extern ID id_is_a;
|
16
|
+
|
17
|
+
//#include <ruby/thread.h>
|
18
|
+
|
19
|
+
namespace h8 {
|
20
|
+
|
21
|
+
template<class T> inline void t(const T& x) {
|
22
|
+
cout << x << endl << flush;
|
23
|
+
}
|
24
|
+
|
25
|
+
class H8 {
|
26
|
+
public:
|
27
|
+
|
28
|
+
class Scope: public HandleScope {
|
29
|
+
v8::Isolate::Scope isolate_scope;
|
30
|
+
v8::Context::Scope context_scope;
|
31
|
+
H8* rcontext;
|
32
|
+
public:
|
33
|
+
Scope(H8* cxt) :
|
34
|
+
HandleScope(cxt->getIsolate()), isolate_scope(
|
35
|
+
cxt->getIsolate()), context_scope(cxt->getContext()), rcontext(
|
36
|
+
cxt) {
|
37
|
+
}
|
38
|
+
};
|
39
|
+
|
40
|
+
static void init();
|
41
|
+
|
42
|
+
H8() {
|
43
|
+
isolate = Isolate::New();
|
44
|
+
Isolate::Scope isolate_scope(isolate);
|
45
|
+
HandleScope handle_scope(isolate);
|
46
|
+
|
47
|
+
v8::Handle<v8::ObjectTemplate> global = v8::ObjectTemplate::New(
|
48
|
+
isolate);
|
49
|
+
v8::Handle<v8::Context> context = v8::Context::New(isolate, NULL,
|
50
|
+
global);
|
51
|
+
persistent_context.Reset(isolate, context);
|
52
|
+
}
|
53
|
+
|
54
|
+
Handle<Value> eval(const char* script_utf) {
|
55
|
+
v8::EscapableHandleScope escape(isolate);
|
56
|
+
Local<Value> result;
|
57
|
+
|
58
|
+
Handle<v8::String> script_source = String::NewFromUtf8(isolate,
|
59
|
+
script_utf);
|
60
|
+
v8::Handle<v8::Script> script;
|
61
|
+
v8::TryCatch try_catch;
|
62
|
+
v8::ScriptOrigin origin(String::NewFromUtf8(isolate, "eval"));
|
63
|
+
|
64
|
+
script = v8::Script::Compile(script_source, &origin);
|
65
|
+
|
66
|
+
if (script.IsEmpty()) {
|
67
|
+
report_exception(try_catch);
|
68
|
+
result = Undefined(isolate);
|
69
|
+
} else {
|
70
|
+
result = script->Run();
|
71
|
+
if (try_catch.HasCaught()) {
|
72
|
+
report_exception(try_catch);
|
73
|
+
}
|
74
|
+
}
|
75
|
+
return escape.Escape(result);
|
76
|
+
}
|
77
|
+
|
78
|
+
VALUE eval_to_ruby(const char* script_utf) {
|
79
|
+
// TODO: throw ruby exception on error
|
80
|
+
return to_ruby(eval(script_utf));
|
81
|
+
}
|
82
|
+
|
83
|
+
Handle<Context> getContext() {
|
84
|
+
return Local<Context>::New(isolate, persistent_context);
|
85
|
+
}
|
86
|
+
|
87
|
+
bool isError() const {
|
88
|
+
return is_error;
|
89
|
+
}
|
90
|
+
|
91
|
+
Isolate* getIsolate() const {
|
92
|
+
return isolate;
|
93
|
+
}
|
94
|
+
|
95
|
+
VALUE to_ruby(Handle<Value> value);
|
96
|
+
|
97
|
+
v8::Local<v8::String> js(VALUE val) const {
|
98
|
+
return js(StringValueCStr(val));
|
99
|
+
}
|
100
|
+
|
101
|
+
v8::Local<v8::String> js(const char* str) const {
|
102
|
+
return v8::String::NewFromUtf8(isolate, str);
|
103
|
+
}
|
104
|
+
|
105
|
+
void set_var(VALUE name, VALUE value) {
|
106
|
+
Scope scope(this);
|
107
|
+
getContext()->Global()->Set(js(name), to_js(value));
|
108
|
+
}
|
109
|
+
|
110
|
+
Local<Value> to_js(VALUE ruby_value) const {
|
111
|
+
switch (TYPE(ruby_value)) {
|
112
|
+
case T_STRING:
|
113
|
+
return js(ruby_value);
|
114
|
+
case T_FIXNUM:
|
115
|
+
return v8::Int32::New(isolate, FIX2INT(ruby_value));
|
116
|
+
case T_FLOAT:
|
117
|
+
return v8::Number::New(isolate, NUM2DBL(ruby_value));
|
118
|
+
case T_DATA:
|
119
|
+
case T_OBJECT:
|
120
|
+
return gateObject(ruby_value);
|
121
|
+
default:
|
122
|
+
rb_raise(h8_exception, "can't gate to js: unknown type");
|
123
|
+
}
|
124
|
+
return Undefined(isolate);
|
125
|
+
}
|
126
|
+
|
127
|
+
Local<Value> gateObject(VALUE object) const;
|
128
|
+
|
129
|
+
virtual ~H8() {
|
130
|
+
persistent_context.Reset();
|
131
|
+
}
|
132
|
+
|
133
|
+
private:
|
134
|
+
friend VALUE context_alloc(VALUE klass);
|
135
|
+
friend void rvalue_mark(void* ptr);
|
136
|
+
|
137
|
+
Isolate *isolate;
|
138
|
+
VALUE self;
|
139
|
+
|
140
|
+
void report_exception(v8::TryCatch& tc) {
|
141
|
+
rb_raise(h8_exception, "Failed to compile/execute script");
|
142
|
+
}
|
143
|
+
|
144
|
+
Persistent<Context> persistent_context;
|
145
|
+
|
146
|
+
bool is_error = false;
|
147
|
+
};
|
148
|
+
// Context
|
149
|
+
}
|
150
|
+
|
151
|
+
typedef VALUE (*ruby_method)(...);
|
152
|
+
|
153
|
+
#include "js_gate.h"
|
154
|
+
|
155
|
+
inline VALUE h8::H8::to_ruby(Handle<Value> value) {
|
156
|
+
return JsGate::to_ruby(this, value);
|
157
|
+
}
|
158
|
+
|
159
|
+
#endif
|
data/ext/h8/js_gate.h
ADDED
@@ -0,0 +1,196 @@
|
|
1
|
+
#ifndef __js_gate_h
|
2
|
+
#define __js_gate_h
|
3
|
+
|
4
|
+
#include "h8.h"
|
5
|
+
|
6
|
+
using namespace v8;
|
7
|
+
|
8
|
+
namespace h8 {
|
9
|
+
|
10
|
+
/**
|
11
|
+
* Interface to anything that could be converted to a Javascipt object. Provides common helpers.
|
12
|
+
*/
|
13
|
+
class JsValue {
|
14
|
+
public:
|
15
|
+
virtual Local<Value> value() const = 0;
|
16
|
+
|
17
|
+
Local<Object> object() const {
|
18
|
+
return value()->ToObject();
|
19
|
+
}
|
20
|
+
|
21
|
+
virtual Isolate* isolate() = 0;
|
22
|
+
};
|
23
|
+
|
24
|
+
|
25
|
+
/**
|
26
|
+
* Gates JS object to ruby environment. Holds persistent reference to the source js object until
|
27
|
+
* ruby object is recycled (then frees it). Note that this ruby object is not meant to be kept alive
|
28
|
+
* by the H8 instance, instead, its owner should.
|
29
|
+
*
|
30
|
+
* Methods of this class do not need the H8::Scope, they create one internally.
|
31
|
+
*/
|
32
|
+
class JsGate : public JsValue {
|
33
|
+
public:
|
34
|
+
/**
|
35
|
+
* Used in the ruby allocator. Do not call unless you know what you do.
|
36
|
+
*/
|
37
|
+
JsGate() {
|
38
|
+
}
|
39
|
+
|
40
|
+
/**
|
41
|
+
* Return Ruby object that gates specified Handled javascript object. Ruby object
|
42
|
+
* locks permanently value until get recycled.
|
43
|
+
*/
|
44
|
+
template <class T>
|
45
|
+
static VALUE to_ruby(H8* h8, const Handle<T>& value) {
|
46
|
+
JsGate *gate;
|
47
|
+
VALUE ruby_gate = rb_class_new_instance(0, NULL, value_class);
|
48
|
+
Data_Get_Struct(ruby_gate, JsGate, gate);
|
49
|
+
gate->set(h8, value);
|
50
|
+
return ruby_gate;
|
51
|
+
}
|
52
|
+
|
53
|
+
/**
|
54
|
+
* Reset gate to the specified handle.
|
55
|
+
*/
|
56
|
+
template<class T>
|
57
|
+
void set(H8 *h8, const Handle<T>& val) {
|
58
|
+
this->h8 = h8;
|
59
|
+
persistent_value.Reset(h8->getIsolate(), val);
|
60
|
+
}
|
61
|
+
|
62
|
+
/**
|
63
|
+
* Get ruby string representation
|
64
|
+
*/
|
65
|
+
VALUE to_s() {
|
66
|
+
H8::Scope scope(h8);
|
67
|
+
String::Utf8Value res(value());
|
68
|
+
return *res ? rb_str_new2(*res) : Qnil;
|
69
|
+
}
|
70
|
+
|
71
|
+
/**
|
72
|
+
* Get ruby integer representation (FIXNUM)
|
73
|
+
*/
|
74
|
+
VALUE to_i() {
|
75
|
+
H8::Scope scope(h8);
|
76
|
+
return INT2FIX(value()->IntegerValue());
|
77
|
+
}
|
78
|
+
|
79
|
+
/**
|
80
|
+
* Get ruby Float representation (FIXNUM)
|
81
|
+
*/
|
82
|
+
VALUE to_f() {
|
83
|
+
H8::Scope scope(h8);
|
84
|
+
return DBL2NUM(value()->NumberValue());
|
85
|
+
}
|
86
|
+
|
87
|
+
/**
|
88
|
+
* @return true if the object is a primitive integer
|
89
|
+
*/
|
90
|
+
VALUE is_int() {
|
91
|
+
H8::Scope scope(h8);
|
92
|
+
return value()->IsInt32() ? Qtrue : Qfalse;
|
93
|
+
}
|
94
|
+
|
95
|
+
/**
|
96
|
+
* @return true if the object is a primitive float
|
97
|
+
*/
|
98
|
+
VALUE is_float() {
|
99
|
+
H8::Scope scope(h8);
|
100
|
+
return value()->IsNumber() ? Qtrue : Qfalse;
|
101
|
+
}
|
102
|
+
|
103
|
+
/**
|
104
|
+
* @return true if the object is an array
|
105
|
+
*/
|
106
|
+
VALUE is_array() {
|
107
|
+
H8::Scope scope(h8);
|
108
|
+
return value()->IsArray() ? Qtrue : Qfalse;
|
109
|
+
}
|
110
|
+
|
111
|
+
/**
|
112
|
+
* @return true if the object is an object
|
113
|
+
*/
|
114
|
+
VALUE is_object() {
|
115
|
+
H8::Scope scope(h8);
|
116
|
+
return value()->IsObject() ? Qtrue : Qfalse;
|
117
|
+
}
|
118
|
+
|
119
|
+
/**
|
120
|
+
* Retreive JS object attribute and convert it to the ruby wrapper
|
121
|
+
* of the new JsGate instace.
|
122
|
+
*/
|
123
|
+
VALUE get_attribute(VALUE name) {
|
124
|
+
H8::Scope scope(h8);
|
125
|
+
Local<Value> v8_name = v8::String::NewFromUtf8(isolate(), StringValueCStr(name));
|
126
|
+
return h8->to_ruby(object()->Get(v8_name));
|
127
|
+
}
|
128
|
+
|
129
|
+
VALUE get_index(VALUE index) {
|
130
|
+
H8::Scope scope(h8);
|
131
|
+
return h8->to_ruby(object()->Get(NUM2INT(index)));
|
132
|
+
}
|
133
|
+
|
134
|
+
/**
|
135
|
+
* @return true if the object is a primitive string
|
136
|
+
*/
|
137
|
+
VALUE is_string() {
|
138
|
+
H8::Scope scope(h8);
|
139
|
+
return value()->IsString() ? Qtrue : Qfalse;
|
140
|
+
}
|
141
|
+
|
142
|
+
VALUE is_function() {
|
143
|
+
H8::Scope scope(h8);
|
144
|
+
return value()->IsFunction();
|
145
|
+
}
|
146
|
+
|
147
|
+
VALUE is_undefined() {
|
148
|
+
H8::Scope scope(h8);
|
149
|
+
return value()->IsUndefined() ? Qtrue : Qfalse;
|
150
|
+
}
|
151
|
+
|
152
|
+
VALUE call(VALUE args) const {
|
153
|
+
v8::HandleScope scope(h8->getIsolate());
|
154
|
+
return apply(h8->getContext()->Global(), args);
|
155
|
+
}
|
156
|
+
|
157
|
+
VALUE apply(VALUE self, VALUE args) const {
|
158
|
+
v8::HandleScope scope(h8->getIsolate());
|
159
|
+
return apply(h8->gateObject(self), args);
|
160
|
+
}
|
161
|
+
|
162
|
+
VALUE apply(Local<Value> self, VALUE args) const {
|
163
|
+
H8::Scope scope(h8);
|
164
|
+
long count = RARRAY_LEN(args);
|
165
|
+
Local<Value> *js_args = new Local<Value>[count];
|
166
|
+
for (int i = 0; i < count; i++) {
|
167
|
+
js_args[i] = h8->to_js(rb_ary_entry(args, i));
|
168
|
+
}
|
169
|
+
Local<Value> result = object()->CallAsFunction(self, count, js_args);
|
170
|
+
delete[] js_args;
|
171
|
+
return h8->to_ruby(result);
|
172
|
+
}
|
173
|
+
|
174
|
+
virtual Local<Value> value() const {
|
175
|
+
return Local<Value>::New(h8->getIsolate(), persistent_value);
|
176
|
+
}
|
177
|
+
|
178
|
+
virtual Isolate* isolate() {
|
179
|
+
return h8->getIsolate();
|
180
|
+
}
|
181
|
+
|
182
|
+
~JsGate() {
|
183
|
+
persistent_value.Reset();
|
184
|
+
}
|
185
|
+
|
186
|
+
private:
|
187
|
+
friend void rvalue_mark(void* ptr);
|
188
|
+
friend class H8;
|
189
|
+
|
190
|
+
H8 *h8=0;
|
191
|
+
Persistent<Value> persistent_value;
|
192
|
+
};
|
193
|
+
|
194
|
+
}
|
195
|
+
|
196
|
+
#endif
|