quickjs 0.1.5 → 0.1.6
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/README.md +21 -5
- data/ext/quickjsrb/extconf.rb +1 -0
- data/ext/quickjsrb/quickjsrb-runtime-state.c +92 -0
- data/ext/quickjsrb/quickjsrb-runtime-state.h +22 -0
- data/ext/quickjsrb/quickjsrb.c +133 -35
- data/lib/quickjs/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6fa772eb6185fa2bc744d7bb7b0cdb2024ccaf2d2ce34cb221139aaf2e819371
|
4
|
+
data.tar.gz: de4b5651b61eaaafe975ba56ae24b954c28efc9ea0ba2a6fb34f149b62d4aa58
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1095f8d04c0c90756506b8bd16a519b207b3b2a0eda64abecec8fb541a0a5a5c0e11240abce18b03e4ee4d7b4c3a53138513218f12afa4d7e46462f3a93680ef
|
7
|
+
data.tar.gz: 3237ad5b23217199900665605629c5b69c0333ea5951f3a43cf2e26ac65d6a718ff94f262fdaeb5bf0c3b132e60cb0561d60a121045a26bb201bbfd63e8e3a9c
|
data/README.md
CHANGED
@@ -40,10 +40,10 @@ Quickjs.eval_code("Number('whatever')") #=> :NaN (Quickjs::Value::NAN)
|
|
40
40
|
|
41
41
|
```rb
|
42
42
|
# 1GB memory limit
|
43
|
-
Quickjs.eval_code(code, {
|
43
|
+
Quickjs.eval_code(code, { memory_limit: 1024 ** 3 })
|
44
44
|
|
45
45
|
# 1MB max stack size
|
46
|
-
Quickjs.eval_code(code, {
|
46
|
+
Quickjs.eval_code(code, { max_stack_size: 1024 ** 2 })
|
47
47
|
```
|
48
48
|
|
49
49
|
#### Enable built-in modules
|
@@ -58,7 +58,7 @@ Quickjs.eval_code(code, { features: [Quickjs::MODULE_STD] })
|
|
58
58
|
Quickjs.eval_code(code, { features: [Quickjs::MODULE_OS] })
|
59
59
|
```
|
60
60
|
|
61
|
-
### Maintain a consistent VM/runtime
|
61
|
+
### `Quickjs::VM`: Maintain a consistent VM/runtime
|
62
62
|
|
63
63
|
```rb
|
64
64
|
vm = Quickjs::VM.new
|
@@ -72,15 +72,20 @@ vm.eval_code('a.b;') #=> "d"
|
|
72
72
|
|
73
73
|
```rb
|
74
74
|
vm = Quickjs::VM.new(
|
75
|
-
memory_limit: 1024
|
76
|
-
max_stack_size: 1024
|
75
|
+
memory_limit: 1024 ** 3,
|
76
|
+
max_stack_size: 1024 ** 2,
|
77
77
|
)
|
78
78
|
```
|
79
79
|
|
80
80
|
```rb
|
81
|
+
# enable std module
|
82
|
+
# https://bellard.org/quickjs/quickjs.html#std-module
|
81
83
|
vm = Quickjs::VM.new(
|
82
84
|
features: [::Quickjs::MODULE_STD],
|
83
85
|
)
|
86
|
+
|
87
|
+
# enable os module
|
88
|
+
# https://bellard.org/quickjs/quickjs.html#os-module
|
84
89
|
vm = Quickjs::VM.new(
|
85
90
|
features: [::Quickjs::MODULE_OS],
|
86
91
|
)
|
@@ -92,6 +97,17 @@ vm = Quickjs::VM.new(
|
|
92
97
|
vm.dispose!
|
93
98
|
```
|
94
99
|
|
100
|
+
#### Define a global function for JS
|
101
|
+
|
102
|
+
```rb
|
103
|
+
vm = Quickjs::VM.new
|
104
|
+
vm.define_function("greetingTo") do |arg1|
|
105
|
+
['Hello!', arg1].join(' ')
|
106
|
+
end
|
107
|
+
|
108
|
+
vm.eval_code("greetingTo('Rick')") #=> 'Hello! Rick'
|
109
|
+
```
|
110
|
+
|
95
111
|
## License
|
96
112
|
|
97
113
|
Every file in `ext/quickjsrb/quickjs` is licensed under [the MIT License Copyright 2017-2021 by Fabrice Bellard and Charlie Goron](/ext/quickjsrb/quickjs/LICENSE).
|
data/ext/quickjsrb/extconf.rb
CHANGED
@@ -0,0 +1,92 @@
|
|
1
|
+
#include "quickjsrb-runtime-state.h"
|
2
|
+
|
3
|
+
unsigned int hash(const char *key) {
|
4
|
+
unsigned long int value = 0;
|
5
|
+
unsigned int i = 0;
|
6
|
+
unsigned int key_len = strlen(key);
|
7
|
+
|
8
|
+
for (; i < key_len; ++i) {
|
9
|
+
value = value * 37 + key[i];
|
10
|
+
}
|
11
|
+
return value % MAX_NUM_OF_PROC;
|
12
|
+
}
|
13
|
+
|
14
|
+
ProcEntry *create_proc_entry(const char *key, VALUE proc) {
|
15
|
+
ProcEntry *entry = malloc(sizeof(ProcEntry));
|
16
|
+
entry->key = strdup(key);
|
17
|
+
entry->proc = proc;
|
18
|
+
entry->next = NULL;
|
19
|
+
return entry;
|
20
|
+
}
|
21
|
+
|
22
|
+
ProcEntryMap *create_proc_entries() {
|
23
|
+
ProcEntryMap *entryMap = malloc(sizeof(ProcEntryMap));
|
24
|
+
entryMap->entries = malloc(sizeof(ProcEntry *) * MAX_NUM_OF_PROC);
|
25
|
+
for (int i = 0; i < MAX_NUM_OF_PROC; ++i) {
|
26
|
+
entryMap->entries[i] = NULL;
|
27
|
+
}
|
28
|
+
return entryMap;
|
29
|
+
}
|
30
|
+
|
31
|
+
void set_proc(ProcEntryMap *entryMap, const char *key, VALUE proc) {
|
32
|
+
unsigned int slot = hash(key);
|
33
|
+
ProcEntry *entry = entryMap->entries[slot];
|
34
|
+
|
35
|
+
if (entry == NULL) {
|
36
|
+
entryMap->entries[slot] = create_proc_entry(key, proc);
|
37
|
+
return;
|
38
|
+
}
|
39
|
+
|
40
|
+
ProcEntry *prev;
|
41
|
+
while (entry != NULL) {
|
42
|
+
if (strcmp(entry->key, key) == 0) {
|
43
|
+
entry->proc = proc;
|
44
|
+
return;
|
45
|
+
}
|
46
|
+
prev = entry;
|
47
|
+
entry = prev->next;
|
48
|
+
}
|
49
|
+
|
50
|
+
prev->next = create_proc_entry(key, proc);
|
51
|
+
}
|
52
|
+
|
53
|
+
VALUE get_proc(ProcEntryMap *entryMap, const char *key) {
|
54
|
+
unsigned int slot = hash(key);
|
55
|
+
|
56
|
+
ProcEntry *entry = entryMap->entries[slot];
|
57
|
+
|
58
|
+
while (entry != NULL) {
|
59
|
+
if (strcmp(entry->key, key) == 0) {
|
60
|
+
return entry->proc;
|
61
|
+
}
|
62
|
+
entry = entry->next;
|
63
|
+
}
|
64
|
+
|
65
|
+
return Qnil;
|
66
|
+
}
|
67
|
+
|
68
|
+
void free_proc_entry_map(ProcEntryMap *entryMap) {
|
69
|
+
for (int i = 0; i < MAX_NUM_OF_PROC; ++i) {
|
70
|
+
ProcEntry *entry = entryMap->entries[i];
|
71
|
+
while (entry != NULL) {
|
72
|
+
ProcEntry *temp = entry;
|
73
|
+
entry = entry->next;
|
74
|
+
free(temp->key);
|
75
|
+
free(temp);
|
76
|
+
}
|
77
|
+
}
|
78
|
+
free(entryMap->entries);
|
79
|
+
free(entryMap);
|
80
|
+
}
|
81
|
+
|
82
|
+
QuickjsrbRuntimeState *create_quickjsrb_runtime_state() {
|
83
|
+
QuickjsrbRuntimeState *state = malloc(sizeof(QuickjsrbRuntimeState));
|
84
|
+
state->procs = create_proc_entries();
|
85
|
+
|
86
|
+
return state;
|
87
|
+
}
|
88
|
+
|
89
|
+
void free_quickjsrb_runtime_state(QuickjsrbRuntimeState *state) {
|
90
|
+
free_proc_entry_map(state->procs);
|
91
|
+
free(state);
|
92
|
+
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
#include "ruby.h"
|
2
|
+
|
3
|
+
#define MAX_NUM_OF_PROC 100
|
4
|
+
|
5
|
+
typedef struct ProcEntry {
|
6
|
+
char *key;
|
7
|
+
VALUE proc;
|
8
|
+
struct ProcEntry *next;
|
9
|
+
} ProcEntry;
|
10
|
+
|
11
|
+
typedef struct ProcEntryMap {
|
12
|
+
ProcEntry **entries;
|
13
|
+
} ProcEntryMap;
|
14
|
+
|
15
|
+
typedef struct QuickjsrbRuntimeState {
|
16
|
+
ProcEntryMap *procs;
|
17
|
+
} QuickjsrbRuntimeState;
|
18
|
+
|
19
|
+
QuickjsrbRuntimeState *create_quickjsrb_runtime_state();
|
20
|
+
VALUE get_proc(ProcEntryMap *entryMap, const char *key);
|
21
|
+
void set_proc(ProcEntryMap *entryMap, const char *key, VALUE proc);
|
22
|
+
void free_quickjsrb_runtime_state(QuickjsrbRuntimeState *state);
|
data/ext/quickjsrb/quickjsrb.c
CHANGED
@@ -1,4 +1,65 @@
|
|
1
1
|
#include "quickjsrb.h"
|
2
|
+
#include "quickjsrb-runtime-state.h"
|
3
|
+
|
4
|
+
struct qvmdata {
|
5
|
+
char alive;
|
6
|
+
struct JSContext *context;
|
7
|
+
struct QuickjsrbRuntimeState *state;
|
8
|
+
};
|
9
|
+
|
10
|
+
void qvm_free(void* data)
|
11
|
+
{
|
12
|
+
free(data);
|
13
|
+
}
|
14
|
+
|
15
|
+
size_t qvm_size(const void* data)
|
16
|
+
{
|
17
|
+
return sizeof(data);
|
18
|
+
}
|
19
|
+
|
20
|
+
static const rb_data_type_t qvm_type = {
|
21
|
+
.wrap_struct_name = "qvm",
|
22
|
+
.function = {
|
23
|
+
.dmark = NULL,
|
24
|
+
.dfree = qvm_free,
|
25
|
+
.dsize = qvm_size,
|
26
|
+
},
|
27
|
+
.data = NULL,
|
28
|
+
.flags = RUBY_TYPED_FREE_IMMEDIATELY,
|
29
|
+
};
|
30
|
+
|
31
|
+
VALUE qvm_alloc(VALUE self)
|
32
|
+
{
|
33
|
+
struct qvmdata *data;
|
34
|
+
|
35
|
+
return TypedData_Make_Struct(self, struct qvmdata, &qvm_type, data);
|
36
|
+
}
|
37
|
+
|
38
|
+
static JSValue js_quickjsrb_call_global (JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
|
39
|
+
struct qvmdata *data = JS_GetContextOpaque(ctx);
|
40
|
+
JSValue maybeFuncName = JS_ToString(ctx, argv[0]);
|
41
|
+
const char *funcName = JS_ToCString(ctx, maybeFuncName);
|
42
|
+
|
43
|
+
VALUE proc = get_proc(data->state->procs, funcName);
|
44
|
+
if (proc == Qnil) {
|
45
|
+
return JS_NewString(ctx, "missing");
|
46
|
+
}
|
47
|
+
JS_FreeValue(ctx, maybeFuncName);
|
48
|
+
|
49
|
+
// TODO: support multiple args
|
50
|
+
JSValue maybeString;
|
51
|
+
VALUE r_array = rb_ary_new2(argc);
|
52
|
+
for (int i = 1; i < argc; ++i) {
|
53
|
+
maybeString = JS_ToString(ctx, argv[i]);
|
54
|
+
const char *msg = JS_ToCString(ctx, maybeString);
|
55
|
+
rb_ary_push(r_array, rb_str_new2(msg));
|
56
|
+
}
|
57
|
+
JS_FreeValue(ctx, maybeString);
|
58
|
+
|
59
|
+
VALUE r_result = rb_funcall(proc, rb_intern("call"), argc - 1, r_array);
|
60
|
+
char *result = StringValueCStr(r_result);
|
61
|
+
return JS_NewString(ctx, result);
|
62
|
+
}
|
2
63
|
|
3
64
|
VALUE rb_mQuickjs;
|
4
65
|
const char *undefinedId = "undefined";
|
@@ -31,6 +92,13 @@ VALUE to_rb_value (JSValue jsv, JSContext *ctx) {
|
|
31
92
|
return rb_str_new2(msg);
|
32
93
|
}
|
33
94
|
case JS_TAG_OBJECT: {
|
95
|
+
int promiseState = JS_PromiseState(ctx, jsv);
|
96
|
+
if (promiseState == JS_PROMISE_FULFILLED || promiseState == JS_PROMISE_PENDING) {
|
97
|
+
return to_rb_value(js_std_await(ctx, jsv), ctx);
|
98
|
+
} else if (promiseState == JS_PROMISE_REJECTED) {
|
99
|
+
return to_rb_value(JS_Throw(ctx, JS_PromiseResult(ctx, jsv)), ctx);
|
100
|
+
}
|
101
|
+
|
34
102
|
JSValue global = JS_GetGlobalObject(ctx);
|
35
103
|
JSValue jsonClass = JS_GetPropertyStr(ctx, global, "JSON");
|
36
104
|
JSValue stringifyFunc = JS_GetPropertyStr(ctx, jsonClass, "stringify");
|
@@ -50,9 +118,28 @@ VALUE to_rb_value (JSValue jsv, JSContext *ctx) {
|
|
50
118
|
return Qnil;
|
51
119
|
case JS_TAG_UNDEFINED:
|
52
120
|
return ID2SYM(rb_intern(undefinedId));
|
53
|
-
case JS_TAG_EXCEPTION:
|
54
|
-
|
121
|
+
case JS_TAG_EXCEPTION: {
|
122
|
+
JSValue exceptionVal = JS_GetException(ctx);
|
123
|
+
if (JS_IsError(ctx, exceptionVal)) {
|
124
|
+
JSValue jsErrorClassName = JS_GetPropertyStr(ctx, exceptionVal, "name");
|
125
|
+
const char *errorClassName = JS_ToCString(ctx, jsErrorClassName);
|
126
|
+
|
127
|
+
JSValue jsErrorClassMessage = JS_GetPropertyStr(ctx, exceptionVal, "message");
|
128
|
+
const char *errorClassMessage = JS_ToCString(ctx, jsErrorClassMessage);
|
129
|
+
|
130
|
+
JS_FreeValue(ctx, jsErrorClassMessage);
|
131
|
+
JS_FreeValue(ctx, jsErrorClassName);
|
132
|
+
|
133
|
+
rb_raise(rb_eRuntimeError, "%s: %s", errorClassName, errorClassMessage);
|
134
|
+
} else {
|
135
|
+
const char *errorMessage = JS_ToCString(ctx, exceptionVal);
|
136
|
+
|
137
|
+
rb_raise(rb_eRuntimeError, "%s", errorMessage);
|
138
|
+
}
|
139
|
+
|
140
|
+
JS_FreeValue(ctx, exceptionVal);
|
55
141
|
return Qnil;
|
142
|
+
}
|
56
143
|
case JS_TAG_BIG_INT: {
|
57
144
|
JSValue toStringFunc = JS_GetPropertyStr(ctx, jsv, "toString");
|
58
145
|
JSValue strigified = JS_Call(ctx, toStringFunc, jsv, 0, NULL);
|
@@ -72,39 +159,6 @@ VALUE to_rb_value (JSValue jsv, JSContext *ctx) {
|
|
72
159
|
}
|
73
160
|
}
|
74
161
|
|
75
|
-
struct qvmdata {
|
76
|
-
struct JSContext *context;
|
77
|
-
char alive;
|
78
|
-
};
|
79
|
-
|
80
|
-
void qvm_free(void* data)
|
81
|
-
{
|
82
|
-
free(data);
|
83
|
-
}
|
84
|
-
|
85
|
-
size_t qvm_size(const void* data)
|
86
|
-
{
|
87
|
-
return sizeof(data);
|
88
|
-
}
|
89
|
-
|
90
|
-
static const rb_data_type_t qvm_type = {
|
91
|
-
.wrap_struct_name = "qvm",
|
92
|
-
.function = {
|
93
|
-
.dmark = NULL,
|
94
|
-
.dfree = qvm_free,
|
95
|
-
.dsize = qvm_size,
|
96
|
-
},
|
97
|
-
.data = NULL,
|
98
|
-
.flags = RUBY_TYPED_FREE_IMMEDIATELY,
|
99
|
-
};
|
100
|
-
|
101
|
-
VALUE qvm_alloc(VALUE self)
|
102
|
-
{
|
103
|
-
struct qvmdata *data;
|
104
|
-
|
105
|
-
return TypedData_Make_Struct(self, struct qvmdata, &qvm_type, data);
|
106
|
-
}
|
107
|
-
|
108
162
|
VALUE qvm_m_initialize(int argc, VALUE* argv, VALUE self)
|
109
163
|
{
|
110
164
|
VALUE r_opts;
|
@@ -120,9 +174,13 @@ VALUE qvm_m_initialize(int argc, VALUE* argv, VALUE self)
|
|
120
174
|
|
121
175
|
struct qvmdata *data;
|
122
176
|
TypedData_Get_Struct(self, struct qvmdata, &qvm_type, data);
|
177
|
+
|
178
|
+
QuickjsrbRuntimeState *state= create_quickjsrb_runtime_state();
|
123
179
|
JSRuntime *runtime = JS_NewRuntime();
|
124
180
|
data->context = JS_NewContext(runtime);
|
181
|
+
data->state = state;
|
125
182
|
data->alive = 1;
|
183
|
+
JS_SetContextOpaque(data->context, data);
|
126
184
|
|
127
185
|
JS_SetMemoryLimit(runtime, NUM2UINT(r_memoryLimit));
|
128
186
|
JS_SetMaxStackSize(runtime, NUM2UINT(r_maxStackSize));
|
@@ -152,6 +210,15 @@ VALUE qvm_m_initialize(int argc, VALUE* argv, VALUE self)
|
|
152
210
|
JS_FreeValue(data->context, osEval);
|
153
211
|
}
|
154
212
|
|
213
|
+
const char *setupGlobalRuby = "globalThis.__ruby = {};\n";
|
214
|
+
JSValue rubyEval = JS_Eval(data->context, setupGlobalRuby, strlen(setupGlobalRuby), "<vm>", JS_EVAL_TYPE_MODULE);
|
215
|
+
JS_FreeValue(data->context, rubyEval);
|
216
|
+
|
217
|
+
JSValue global = JS_GetGlobalObject(data->context);
|
218
|
+
JSValue func = JS_NewCFunction(data->context, js_quickjsrb_call_global, "rubyGlobal", 2);
|
219
|
+
JS_SetPropertyStr(data->context, global, "rubyGlobal", func);
|
220
|
+
JS_FreeValue(data->context, global);
|
221
|
+
|
155
222
|
return self;
|
156
223
|
}
|
157
224
|
|
@@ -172,6 +239,35 @@ VALUE qvm_m_evalCode(VALUE self, VALUE r_code)
|
|
172
239
|
return result;
|
173
240
|
}
|
174
241
|
|
242
|
+
VALUE qvm_m_defineGlobalFunction(VALUE self, VALUE r_name)
|
243
|
+
{
|
244
|
+
rb_need_block();
|
245
|
+
|
246
|
+
struct qvmdata *data;
|
247
|
+
TypedData_Get_Struct(self, struct qvmdata, &qvm_type, data);
|
248
|
+
|
249
|
+
if (rb_block_given_p()) {
|
250
|
+
VALUE proc = rb_block_proc();
|
251
|
+
|
252
|
+
char *funcName = StringValueCStr(r_name);
|
253
|
+
|
254
|
+
set_proc(data->state->procs, funcName, proc);
|
255
|
+
|
256
|
+
const char* template = "globalThis.__ruby['%s'] = (...args) => rubyGlobal('%s', args);\nglobalThis['%s'] = globalThis.__ruby['%s'];\n";
|
257
|
+
int length = snprintf(NULL, 0, template, funcName, funcName, funcName, funcName);
|
258
|
+
char* result = (char*)malloc(length + 1);
|
259
|
+
snprintf(result, length + 1, template, funcName, funcName, funcName, funcName);
|
260
|
+
|
261
|
+
JSValue codeResult = JS_Eval(data->context, result, strlen(result), "<vm>", JS_EVAL_TYPE_MODULE);
|
262
|
+
|
263
|
+
JS_FreeValue(data->context, codeResult);
|
264
|
+
free(result);
|
265
|
+
return rb_funcall(r_name, rb_intern("to_sym"), 0, NULL);
|
266
|
+
}
|
267
|
+
|
268
|
+
return Qnil;
|
269
|
+
}
|
270
|
+
|
175
271
|
VALUE qvm_m_dispose(VALUE self)
|
176
272
|
{
|
177
273
|
struct qvmdata *data;
|
@@ -179,6 +275,7 @@ VALUE qvm_m_dispose(VALUE self)
|
|
179
275
|
|
180
276
|
JSRuntime *runtime = JS_GetRuntime(data->context);
|
181
277
|
js_std_free_handlers(runtime);
|
278
|
+
free_quickjsrb_runtime_state(data->state);
|
182
279
|
JS_FreeContext(data->context);
|
183
280
|
JS_FreeRuntime(runtime);
|
184
281
|
data->alive = 0;
|
@@ -201,5 +298,6 @@ Init_quickjsrb(void)
|
|
201
298
|
rb_define_alloc_func(vmClass, qvm_alloc);
|
202
299
|
rb_define_method(vmClass, "initialize", qvm_m_initialize, -1);
|
203
300
|
rb_define_method(vmClass, "eval_code", qvm_m_evalCode, 1);
|
301
|
+
rb_define_method(vmClass, "define_function", qvm_m_defineGlobalFunction, 1);
|
204
302
|
rb_define_method(vmClass, "dispose!", qvm_m_dispose, 0);
|
205
303
|
}
|
data/lib/quickjs/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: quickjs
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- hmsk
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-07-
|
11
|
+
date: 2024-07-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json
|
@@ -62,6 +62,8 @@ files:
|
|
62
62
|
- ext/quickjsrb/quickjs/run-test262.c
|
63
63
|
- ext/quickjsrb/quickjs/unicode_gen.c
|
64
64
|
- ext/quickjsrb/quickjs/unicode_gen_def.h
|
65
|
+
- ext/quickjsrb/quickjsrb-runtime-state.c
|
66
|
+
- ext/quickjsrb/quickjsrb-runtime-state.h
|
65
67
|
- ext/quickjsrb/quickjsrb.c
|
66
68
|
- ext/quickjsrb/quickjsrb.h
|
67
69
|
- lib/quickjs.rb
|