jscall 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +12 -0
- data/LICENSE +21 -0
- data/README.md +260 -0
- data/Rakefile +13 -0
- data/jscall.gemspec +38 -0
- data/lib/jscall/apple-touch-icon.png +0 -0
- data/lib/jscall/browser.mjs +67 -0
- data/lib/jscall/browser.rb +153 -0
- data/lib/jscall/favicon.ico +0 -0
- data/lib/jscall/jscall.html +22 -0
- data/lib/jscall/main.mjs +340 -0
- data/lib/jscall/version.rb +7 -0
- data/lib/jscall.rb +344 -0
- metadata +71 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 2c3d95c5cc2e431807e613c0ba1f3fa4c7bc2d74b14974bd780eb787e9db4d13
|
4
|
+
data.tar.gz: 6a301b76f12d69fde106d923f0f22e355e38450c1f4c1d7673508984c222d64f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 572d0824bc9d2058a4016e28a469fec90bf718124ce4fdd647ef4543fca98b5d6f5b2e43ff06aa2bc11bdcf898cfb0cd7655ef0d09e4ac60263f8c689e72bc6d
|
7
|
+
data.tar.gz: 4a7ce3b1340f3df751c6f5b5467d3242fe53e0a557018d99a0b4475855f54661de8c638376de8e2c4d0a38609f1e9acf9de91dd3ecb25c0dca6534ba9de211d8
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2022- Shigeru Chiba.
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,260 @@
|
|
1
|
+
# Jscall
|
2
|
+
|
3
|
+
Jscall allows executing a program in JavaScript on node.js or a web browser.
|
4
|
+
By default, node.js is used for the execution.
|
5
|
+
To choose a web browser, call `Jscall.config`.
|
6
|
+
|
7
|
+
```
|
8
|
+
Jscall.config(browser: true)
|
9
|
+
```
|
10
|
+
|
11
|
+
To run JavaScript code, call `Jscall.exec`.
|
12
|
+
For example,
|
13
|
+
|
14
|
+
```
|
15
|
+
Jscall.exec '1 + 1'
|
16
|
+
```
|
17
|
+
|
18
|
+
This returns `2`. The argument passed to `Jscall.exec` can be
|
19
|
+
multipe lines. It is executed as source code written in JavaScript.
|
20
|
+
|
21
|
+
`Jscall.exec` returns a resulting value. Numbers, character strings (and symbols), boolean values, and `nil` (and `null`)
|
22
|
+
are copied when passing between Ruby and JavaScript. An array is also shallow-copied.
|
23
|
+
Other objects are not copied. When they are passed, a remote reference is created at the destination.
|
24
|
+
|
25
|
+
A remote reference is a proxy object. A method call on a remote reference invokes a method on the corresponding object on the remote site. For example,
|
26
|
+
|
27
|
+
```
|
28
|
+
js_obj = Jscall.exec '({ foo: (x) => x + 1, bar: 7 })'
|
29
|
+
js_obj.foo(3) # 4
|
30
|
+
js_obj.bar # 7
|
31
|
+
js_obj.baz = 9
|
32
|
+
js_obj.baz # 9
|
33
|
+
```
|
34
|
+
|
35
|
+
The `foo` method is executed in JavaScript.
|
36
|
+
Since `bar` is not a function, its value is returned to Ruby as it is.
|
37
|
+
|
38
|
+
Setting a object property to a given value is also
|
39
|
+
allowed. The expression `js_obj.baz = 9` above sets
|
40
|
+
the object property `baz` to 9.
|
41
|
+
|
42
|
+
To call a JavaScript function from Ruby, call a mehtod on `Jscall`.
|
43
|
+
For example,
|
44
|
+
|
45
|
+
```
|
46
|
+
Jscall.exec <<CODE
|
47
|
+
function foo(x) {
|
48
|
+
return x + 1
|
49
|
+
}
|
50
|
+
CODE
|
51
|
+
Jscall.foo(7) # 8
|
52
|
+
```
|
53
|
+
|
54
|
+
`Jscall.foo(7)` invokes the JavaScript function with the name following `Jscall.`
|
55
|
+
with the given argument. In this case,
|
56
|
+
the `foo` function is executed with the argument `7`.
|
57
|
+
|
58
|
+
When a Ruby object is passed to a JavaScript function/method,
|
59
|
+
it can call a method on the passed Ruby object.
|
60
|
+
|
61
|
+
```
|
62
|
+
Jscall.exec <<CODE
|
63
|
+
async function foo(obj) {
|
64
|
+
return await obj.to_a()
|
65
|
+
}
|
66
|
+
CODE
|
67
|
+
Jscall.foo((1..3)) # [], 2, 3]
|
68
|
+
```
|
69
|
+
|
70
|
+
Here, `obj.to_a()` calls the `to_a` method on a `Range` object
|
71
|
+
created in Ruby.
|
72
|
+
Note that you must `await` every call to Ruby object since it is
|
73
|
+
asynchronous call.
|
74
|
+
|
75
|
+
In JavaScript, `Ruby.exec` is availale to run a program in Ruby.
|
76
|
+
For example,
|
77
|
+
|
78
|
+
```
|
79
|
+
Jscall.exec <<CODE
|
80
|
+
async function foo() {
|
81
|
+
return await Ruby.exec('RUBY_VERSION')
|
82
|
+
}
|
83
|
+
CODE
|
84
|
+
Jscall.foo()
|
85
|
+
```
|
86
|
+
|
87
|
+
`Jscall.foo()` returns the result of evaluating `RUBY_VERSION`
|
88
|
+
in Ruby.
|
89
|
+
Don't forget to `await` a call to `Ruby.exec`.
|
90
|
+
|
91
|
+
Remote references will be automatically reclaimed when they are no
|
92
|
+
longer used. To reclaim them immediately, call:
|
93
|
+
|
94
|
+
```
|
95
|
+
Jscall.scavenge_references
|
96
|
+
```
|
97
|
+
|
98
|
+
## DOM manipulation
|
99
|
+
|
100
|
+
When JavaScript code is run on a browser, some utility methods
|
101
|
+
are available in Ruby for manipulating DOM objects.
|
102
|
+
|
103
|
+
- `Jscall.dom.append_css(css_file_path)`
|
104
|
+
|
105
|
+
This adds a `link` element to the DOM tree so that the specified
|
106
|
+
css file will be linked. For example, `append_css('/mystyle.css')`
|
107
|
+
links `mystyle.css` in the current directory.
|
108
|
+
|
109
|
+
- `Jscall.dom.print(msg)`
|
110
|
+
|
111
|
+
This adds a `p` element to the DOM tree.
|
112
|
+
|
113
|
+
|
114
|
+
## Variable scope
|
115
|
+
|
116
|
+
Since Jscall uses `eval` to execute JavaScript code, the scope of
|
117
|
+
variable/constant names is within the code passed to `eval`.
|
118
|
+
For example,
|
119
|
+
|
120
|
+
```
|
121
|
+
Jscall.exec 'const k = 3'
|
122
|
+
Jscall.exec 'k + 3' # Can't find variable: k
|
123
|
+
```
|
124
|
+
|
125
|
+
The second line causes an error. `k` is not visible
|
126
|
+
when `'k + 3'` is executed.
|
127
|
+
|
128
|
+
To avoid this, use a global variable.
|
129
|
+
|
130
|
+
```
|
131
|
+
Jscall.exec 'k = 3'
|
132
|
+
Jscall.exec 'globalThis.j = 4'
|
133
|
+
Jscall.exec 'k + j' # 7
|
134
|
+
```
|
135
|
+
|
136
|
+
## Loading a module
|
137
|
+
|
138
|
+
When JavaScript code is executed on node.js, `require` is available in JavaScript
|
139
|
+
for loading a CommonJS module. For example,
|
140
|
+
|
141
|
+
```
|
142
|
+
Jscall.exec "mime = require('./mime.js')"
|
143
|
+
```
|
144
|
+
|
145
|
+
The file `./mime.js` is loaded and the module is bound to a global variable `mime`.
|
146
|
+
|
147
|
+
You may want to call `Jscall.dyn_import` in Ruby.
|
148
|
+
|
149
|
+
```
|
150
|
+
fs = Jscall.dyn_import('fs')
|
151
|
+
```
|
152
|
+
|
153
|
+
This executes dynamic importing in JavaScript.
|
154
|
+
For node.js, the file name of the imported module should be a full path name. For a web browser, the root directory is a current working directory. So `Jscall.dyn_import('/mine.mjs')` loads the file `./mine.mjs`.
|
155
|
+
|
156
|
+
`Jscall.dyn_import` takes the second argument. If it is given,
|
157
|
+
a global variable in JavaScript is bound to the loaded module.
|
158
|
+
|
159
|
+
```
|
160
|
+
fs = Jscall.dyn_import('fs', 'fs_module')
|
161
|
+
```
|
162
|
+
|
163
|
+
This is quite equivalent to the following JavaScript code:
|
164
|
+
|
165
|
+
```
|
166
|
+
fs_module = await load('fs')
|
167
|
+
```
|
168
|
+
|
169
|
+
|
170
|
+
## Configuration
|
171
|
+
|
172
|
+
To import JavaScript modules when node.js starts,
|
173
|
+
|
174
|
+
```
|
175
|
+
Jscall.config(module_names: [["Foo", "./foo.mjs"], ["Bar", "./bar.mjs"]], options: "--use-strict")
|
176
|
+
```
|
177
|
+
|
178
|
+
This specifies that `./foo.mjs` and `./bar.mjs` are impoted when node.js starts.
|
179
|
+
This is equivalent to the following import declarations:
|
180
|
+
|
181
|
+
```
|
182
|
+
import * as "Foo" from "./foo.mjs"
|
183
|
+
import * as "Bar" from "./bar.mjs"
|
184
|
+
```
|
185
|
+
|
186
|
+
`'--use-strict'` is passed to node.js as a command line argument.
|
187
|
+
|
188
|
+
`module_names:` and `options:` are optional arguments to `Jscall.config`.
|
189
|
+
|
190
|
+
To change the name of the node command,
|
191
|
+
|
192
|
+
```
|
193
|
+
Jscall::PipeToJs.node_command = "node.exe"
|
194
|
+
```
|
195
|
+
|
196
|
+
The default command name is `"node"`.
|
197
|
+
|
198
|
+
When running JavaScript code on a web browser,
|
199
|
+
|
200
|
+
```
|
201
|
+
Jscall.config(browser: true, options: {port: 10082})
|
202
|
+
```
|
203
|
+
|
204
|
+
`options:` is an optional argument.
|
205
|
+
The example above specifies that Ruby receives http requests
|
206
|
+
sent to http://localhost:10082 from JavaScript on a web browser.
|
207
|
+
|
208
|
+
Passing `false` for `browser:` to `Jscall.config` switches
|
209
|
+
the execution engine to node.js.
|
210
|
+
Call `Jscall.close` to detach the current execution engine.
|
211
|
+
A new enigine with a new configuration will be created.
|
212
|
+
|
213
|
+
To change the command for launching a web browser,
|
214
|
+
|
215
|
+
```
|
216
|
+
Jscall::FetchServer.open_command = "open -a '/Applications/Safari.app'"
|
217
|
+
```
|
218
|
+
|
219
|
+
By default, the command name is `open` for macOS, `start` for Windows,
|
220
|
+
or `xdg-open` for Linux.
|
221
|
+
Jscall launches a web browser by the command like the following:
|
222
|
+
|
223
|
+
```
|
224
|
+
open http://localhost:10082/jscall/jscall.html
|
225
|
+
```
|
226
|
+
|
227
|
+
## Installation
|
228
|
+
|
229
|
+
Add this line to your application's Gemfile:
|
230
|
+
|
231
|
+
```ruby
|
232
|
+
gem 'jscall'
|
233
|
+
```
|
234
|
+
|
235
|
+
And then execute:
|
236
|
+
|
237
|
+
$ bundle install
|
238
|
+
|
239
|
+
Or install it yourself as:
|
240
|
+
|
241
|
+
$ gem install jscall
|
242
|
+
|
243
|
+
## Development
|
244
|
+
|
245
|
+
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
246
|
+
|
247
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
248
|
+
|
249
|
+
## Contributing
|
250
|
+
|
251
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/csg-tokyo/jscall.
|
252
|
+
|
253
|
+
## License
|
254
|
+
|
255
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
256
|
+
|
257
|
+
## Acknowledgement
|
258
|
+
|
259
|
+
The icon image for jscall was created by partly using the Ruby logo, which was obtained
|
260
|
+
from https://www.ruby-lang.org/en/about/logo/ under CC BY-SA 2.5.
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rake/testtask"
|
5
|
+
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
7
|
+
t.libs << "test"
|
8
|
+
t.libs << "lib"
|
9
|
+
t.test_files = FileList["test/**/test_*.rb"]
|
10
|
+
end
|
11
|
+
|
12
|
+
task default: :test
|
13
|
+
# task default: %i[]
|
data/jscall.gemspec
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/jscall/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "jscall"
|
7
|
+
spec.version = Jscall::VERSION
|
8
|
+
spec.authors = ["Shigeru Chiba"]
|
9
|
+
spec.email = ["?"]
|
10
|
+
|
11
|
+
spec.summary = "a library for calling JavaScript functions"
|
12
|
+
spec.description = "a library for executing JavaScript code on node.js or a web browser"
|
13
|
+
spec.homepage = "https://github.com/csg-tokyo/jscall"
|
14
|
+
spec.required_ruby_version = ">= 2.6.0"
|
15
|
+
|
16
|
+
# spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
|
17
|
+
|
18
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
19
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
20
|
+
# spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
|
21
|
+
|
22
|
+
# Specify which files should be added to the gem when it is released.
|
23
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
24
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
25
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
26
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
27
|
+
end
|
28
|
+
end
|
29
|
+
spec.bindir = "exe"
|
30
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
31
|
+
spec.require_paths = ["lib"]
|
32
|
+
|
33
|
+
# Uncomment to register a new dependency of your gem
|
34
|
+
spec.add_dependency "webrick", "~> 1.4"
|
35
|
+
|
36
|
+
# For more information and examples about making a new gem, check out our
|
37
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
38
|
+
end
|
Binary file
|
@@ -0,0 +1,67 @@
|
|
1
|
+
// Copyright (C) 2022- Shigeru Chiba. All rights reserved.
|
2
|
+
|
3
|
+
export class HttpStream {
|
4
|
+
constructor() {
|
5
|
+
this.send_buffer = ['start']
|
6
|
+
this.send_callback = null
|
7
|
+
}
|
8
|
+
|
9
|
+
[Symbol.asyncIterator]() {
|
10
|
+
const http_stream = this
|
11
|
+
return {
|
12
|
+
next() {
|
13
|
+
let msg = http_stream.send_buffer.shift()
|
14
|
+
if (msg === undefined)
|
15
|
+
return new Promise((resolve, reject) => {
|
16
|
+
if (http_stream.send_callback === null)
|
17
|
+
http_stream.send_callback = resolve
|
18
|
+
else
|
19
|
+
throw new Error('(fatal) send_callback is not null!')
|
20
|
+
}).then(() => this.next())
|
21
|
+
else
|
22
|
+
return http_stream.do_fetch(msg)
|
23
|
+
}
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
27
|
+
do_fetch(msg) {
|
28
|
+
const hs = new Headers()
|
29
|
+
hs.append('Content-Type', 'text/plain')
|
30
|
+
return fetch('/cmd/', { method: 'POST', headers: hs, body: msg })
|
31
|
+
.then(async (response) => {
|
32
|
+
if (response.ok) {
|
33
|
+
const text = await response.text()
|
34
|
+
return { value: text, done: text === 'done' }
|
35
|
+
}
|
36
|
+
else
|
37
|
+
throw new Error(`HTTP error! Status: ${response.status}`)
|
38
|
+
},
|
39
|
+
(reason) => { return { value: 'failure', done: true } })
|
40
|
+
}
|
41
|
+
|
42
|
+
puts(msg) {
|
43
|
+
this.send_buffer.push(msg)
|
44
|
+
if (this.send_callback !== null) {
|
45
|
+
const callback = this.send_callback
|
46
|
+
this.send_callback = null
|
47
|
+
callback()
|
48
|
+
}
|
49
|
+
}
|
50
|
+
|
51
|
+
setEncoding(encoding) {}
|
52
|
+
}
|
53
|
+
|
54
|
+
export const Jscall = new class {
|
55
|
+
print(msg) {
|
56
|
+
const e = document.createElement('p')
|
57
|
+
e.textContent = msg
|
58
|
+
document.body.append(e)
|
59
|
+
}
|
60
|
+
|
61
|
+
append_css(file_name) {
|
62
|
+
const link = document.createElement('link')
|
63
|
+
link.rel = "stylesheet"
|
64
|
+
link.href = file_name
|
65
|
+
document.head.append(link)
|
66
|
+
}
|
67
|
+
}
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (C) 2022- Shigeru Chiba. All rights reserved.
|
4
|
+
|
5
|
+
require 'webrick'
|
6
|
+
|
7
|
+
module Jscall
|
8
|
+
WEBrick::HTTPUtils::DefaultMimeTypes['mjs'] ||= "application/javascript"
|
9
|
+
|
10
|
+
class Dom
|
11
|
+
def method_missing(name, *args)
|
12
|
+
Jscall.__getpipe__.funcall(nil, "Jscall.#{name}", args)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
@js_dom = Dom.new
|
17
|
+
|
18
|
+
def self.dom
|
19
|
+
@js_dom
|
20
|
+
end
|
21
|
+
|
22
|
+
class PipeToJs
|
23
|
+
end
|
24
|
+
|
25
|
+
class PipeToBrowser < PipeToJs
|
26
|
+
def startJS(module_names, options)
|
27
|
+
port = 10081
|
28
|
+
port = options[:port] if options.is_a?(Hash) && options.has_key?(:port)
|
29
|
+
@pipe = FetchServer.new(port)
|
30
|
+
@pipe.open
|
31
|
+
end
|
32
|
+
|
33
|
+
def close
|
34
|
+
@pipe.shutdown
|
35
|
+
sleep(0.5)
|
36
|
+
true
|
37
|
+
end
|
38
|
+
|
39
|
+
def soft_close
|
40
|
+
@pipe.close
|
41
|
+
false
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class FetchServer
|
46
|
+
@@webpage = '/jscall/jscall.html'
|
47
|
+
|
48
|
+
@@run_cmd = case RbConfig::CONFIG['host_os']
|
49
|
+
when /linux/
|
50
|
+
'xdg-open'
|
51
|
+
when /darwin|mac os/
|
52
|
+
'open'
|
53
|
+
else
|
54
|
+
'start'
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.open_command=(name)
|
58
|
+
@@run_cmd = name
|
59
|
+
end
|
60
|
+
|
61
|
+
def initialize(port)
|
62
|
+
@send_buffer = Thread::Queue.new
|
63
|
+
@receive_buffer = Thread::Queue.new
|
64
|
+
@server_running = false
|
65
|
+
|
66
|
+
@server = WEBrick::HTTPServer.new(
|
67
|
+
# :DoNotReverseLookup => true,
|
68
|
+
:DocumentRoot => './',
|
69
|
+
:BindAddress => '0.0.0.0',
|
70
|
+
:Port => port,
|
71
|
+
:ServerType => Thread,
|
72
|
+
:Logger => WEBrick::Log.new(nil, WEBrick::Log::ERROR),
|
73
|
+
:AccessLog => []
|
74
|
+
)
|
75
|
+
|
76
|
+
@server.mount_proc('/') do |req, res|
|
77
|
+
peer_address = req.peeraddr[3]
|
78
|
+
if peer_address != '127.0.0.1'
|
79
|
+
$stderr.puts "access denied address=#{peer_address}"
|
80
|
+
raise WEBrick::HTTPStatus::Forbidden
|
81
|
+
end
|
82
|
+
|
83
|
+
if req.path.start_with?('/cmd/')
|
84
|
+
read_stream(req, res)
|
85
|
+
else
|
86
|
+
read_file(req, res)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def read_stream(req, res)
|
92
|
+
body = req.body
|
93
|
+
@receive_buffer.push(if body.nil? then '' else body end)
|
94
|
+
res.content_type = "text/plain"
|
95
|
+
res.body = @send_buffer.pop
|
96
|
+
end
|
97
|
+
|
98
|
+
def read_file(req, res)
|
99
|
+
if req.path.start_with?('/jscall/')
|
100
|
+
root = "#{__dir__}/../"
|
101
|
+
else
|
102
|
+
root = @server.config[:DocumentRoot]
|
103
|
+
end
|
104
|
+
WEBrick::HTTPServlet::FileHandler.new(@server, root).service(req, res)
|
105
|
+
end
|
106
|
+
|
107
|
+
def open
|
108
|
+
if @server_running
|
109
|
+
close
|
110
|
+
else
|
111
|
+
@server_running = true
|
112
|
+
if @server.status != :Running
|
113
|
+
Signal.trap(:INT){ @server.shutdown }
|
114
|
+
@server.start
|
115
|
+
Thread.pass
|
116
|
+
end
|
117
|
+
end
|
118
|
+
raise "A web page was reloaded" unless @receive_buffer.empty?
|
119
|
+
status = system "#{@@run_cmd} http://localhost:#{@server[:Port]}#{@@webpage}"
|
120
|
+
raise "cannot launch a web browser by '#{@@run_cmd}'" if status.nil?
|
121
|
+
unless @receive_buffer.pop == 'start'
|
122
|
+
raise 'failed to initialize JavaScript'
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def close
|
127
|
+
puts('done')
|
128
|
+
@server_running = false
|
129
|
+
end
|
130
|
+
|
131
|
+
def closed?
|
132
|
+
!@server_running
|
133
|
+
end
|
134
|
+
|
135
|
+
def autoclose=(value)
|
136
|
+
false
|
137
|
+
end
|
138
|
+
|
139
|
+
def shutdown
|
140
|
+
close
|
141
|
+
@server.stop
|
142
|
+
end
|
143
|
+
|
144
|
+
def puts(msg)
|
145
|
+
@send_buffer.push(msg)
|
146
|
+
Thread.pass
|
147
|
+
end
|
148
|
+
|
149
|
+
def gets
|
150
|
+
@receive_buffer.pop
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
Binary file
|
@@ -0,0 +1,22 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8">
|
5
|
+
<title>Jscall</title>
|
6
|
+
<link rel="icon" type="image/png" sizes="32x32" href="./favicon.ico">
|
7
|
+
<link rel="apple-touch-icon" href="./apple-touch-icon.png">
|
8
|
+
|
9
|
+
<script type="module" src="./main.mjs"></script>
|
10
|
+
<script type="module" src="./browser.mjs"></script>
|
11
|
+
<script type="module">
|
12
|
+
import * as Ruby from './main.mjs'
|
13
|
+
import { HttpStream, Jscall } from './browser.mjs'
|
14
|
+
globalThis.Ruby = Ruby
|
15
|
+
globalThis.Jscall = Jscall
|
16
|
+
Ruby.start(new HttpStream(), false)
|
17
|
+
</script>
|
18
|
+
</head>
|
19
|
+
|
20
|
+
<body>
|
21
|
+
</body>
|
22
|
+
</html>
|
data/lib/jscall/main.mjs
ADDED
@@ -0,0 +1,340 @@
|
|
1
|
+
// Copyright (C) 2022- Shigeru Chiba. All rights reserved.
|
2
|
+
|
3
|
+
const cmd_eval = 1
|
4
|
+
const cmd_call = 2
|
5
|
+
const cmd_reply = 3
|
6
|
+
const param_array = 0
|
7
|
+
const param_object = 1
|
8
|
+
const param_local_object = 2
|
9
|
+
const param_error = 3
|
10
|
+
|
11
|
+
const table_size = 100
|
12
|
+
|
13
|
+
const exported = new class {
|
14
|
+
constructor() {
|
15
|
+
this.objects = Array(table_size).fill().map((_, i) => i + 1)
|
16
|
+
this.objects[table_size - 1] = -1
|
17
|
+
this.free_element = 0
|
18
|
+
this.hashtable = new Map()
|
19
|
+
}
|
20
|
+
|
21
|
+
export(obj) {
|
22
|
+
const idx = this.hashtable.get(obj)
|
23
|
+
if (idx !== undefined)
|
24
|
+
return idx
|
25
|
+
else {
|
26
|
+
const idx = this.next_element()
|
27
|
+
this.objects[idx] = obj
|
28
|
+
this.hashtable.set(obj, idx)
|
29
|
+
return idx
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|
33
|
+
export_remoteref(prref) { // proxy for remote reference
|
34
|
+
return prref.__self__.id
|
35
|
+
}
|
36
|
+
|
37
|
+
next_element() {
|
38
|
+
const idx = this.free_element
|
39
|
+
if (idx < 0)
|
40
|
+
return this.objects.length
|
41
|
+
else {
|
42
|
+
this.free_element = this.objects[idx]
|
43
|
+
return idx
|
44
|
+
}
|
45
|
+
}
|
46
|
+
|
47
|
+
find(idx) {
|
48
|
+
const obj = this.objects[idx]
|
49
|
+
if (typeof obj === 'number')
|
50
|
+
throw `unknown index is given to find(): ${idx}`
|
51
|
+
else
|
52
|
+
return obj
|
53
|
+
}
|
54
|
+
|
55
|
+
remove(idx) {
|
56
|
+
const obj = this.objects[idx]
|
57
|
+
if (typeof obj === 'number')
|
58
|
+
throw `unknown index is given to remove(): ${idx}`
|
59
|
+
else {
|
60
|
+
this.objects[idx] = this.free_element
|
61
|
+
this.free_element = idx
|
62
|
+
this.hashtable.delete(obj)
|
63
|
+
}
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
const RemoteRef = class {
|
68
|
+
constructor(id) {
|
69
|
+
this.id = id
|
70
|
+
}
|
71
|
+
}
|
72
|
+
|
73
|
+
const remoteRefHandler = new class {
|
74
|
+
get(obj, name) {
|
75
|
+
if (name === '__self__')
|
76
|
+
return obj
|
77
|
+
else
|
78
|
+
return (...args) => {
|
79
|
+
// this returns Promise
|
80
|
+
return funcall_to_ruby(obj.id, name, args)
|
81
|
+
}
|
82
|
+
}
|
83
|
+
}
|
84
|
+
|
85
|
+
const imported = new class {
|
86
|
+
constructor() {
|
87
|
+
this.objects = Array(table_size).fill(null)
|
88
|
+
this.canary = new WeakRef(new RemoteRef(-1))
|
89
|
+
}
|
90
|
+
|
91
|
+
import(index) {
|
92
|
+
const wref = this.objects[index]
|
93
|
+
const obj = wref === null || wref === undefined ? null : wref.deref()
|
94
|
+
if (obj !== null && obj !== undefined)
|
95
|
+
return obj
|
96
|
+
else {
|
97
|
+
const rref = new Proxy(new RemoteRef(index), remoteRefHandler)
|
98
|
+
this.objects[index] = new WeakRef(rref)
|
99
|
+
return rref
|
100
|
+
}
|
101
|
+
}
|
102
|
+
|
103
|
+
kill_canary() { this.canary = null }
|
104
|
+
|
105
|
+
// collect reclaimed RemoteRef objects.
|
106
|
+
dead_references() {
|
107
|
+
if (this.canary === null || this.canary.deref() === undefined)
|
108
|
+
this.canary = new WeakRef(new RemoteRef(-1))
|
109
|
+
else
|
110
|
+
return [] // the canary is alive, so no GC has happened yet.
|
111
|
+
|
112
|
+
const deads = []
|
113
|
+
this.objects.forEach((wref, index) => {
|
114
|
+
if (wref !== null && wref !== undefined && wref.deref() === undefined) {
|
115
|
+
this.objects[index] = null
|
116
|
+
deads.push(index)
|
117
|
+
}
|
118
|
+
})
|
119
|
+
return deads
|
120
|
+
}
|
121
|
+
}
|
122
|
+
|
123
|
+
const encode_obj = obj => {
|
124
|
+
if (typeof obj === 'number' || typeof obj === 'string' || obj === true || obj === false || obj === null)
|
125
|
+
return obj
|
126
|
+
else if (obj === undefined)
|
127
|
+
return null
|
128
|
+
else if (obj.constructor === Array)
|
129
|
+
return [param_array, obj.map(e => encode_obj(e))]
|
130
|
+
else if (obj instanceof RemoteRef)
|
131
|
+
return [param_local_object, exported.export_remoteref(obj)]
|
132
|
+
else
|
133
|
+
return [param_object, exported.export(obj)]
|
134
|
+
}
|
135
|
+
|
136
|
+
const encode_error = msg => [param_error, msg]
|
137
|
+
|
138
|
+
const RubyError = class {
|
139
|
+
constructor(msg) { this.message = msg }
|
140
|
+
get() { return 'RubyError: ' + this.message }
|
141
|
+
}
|
142
|
+
|
143
|
+
const decode_obj = obj => {
|
144
|
+
if (typeof obj === 'number' || typeof obj === 'string' || obj === true || obj === false || obj === null)
|
145
|
+
return obj
|
146
|
+
else if (obj.constructor === Array && obj.length == 2)
|
147
|
+
if (obj[0] == param_array)
|
148
|
+
return obj[1].map(e => decode_obj(e))
|
149
|
+
else if (obj[0] == param_object)
|
150
|
+
return imported.import(obj[1])
|
151
|
+
else if (obj[0] == param_local_object)
|
152
|
+
return exported.find(obj[1])
|
153
|
+
else if (obj[0] == param_error)
|
154
|
+
return new RubyError(obj[1])
|
155
|
+
|
156
|
+
throw `decode_obj: unsupported value, ${obj}`
|
157
|
+
}
|
158
|
+
|
159
|
+
const js_eval = eval
|
160
|
+
|
161
|
+
const funcall_from_ruby = cmd => {
|
162
|
+
const receiver = decode_obj(cmd[1])
|
163
|
+
const name = cmd[2]
|
164
|
+
const args = cmd[3].map(e => decode_obj(e))
|
165
|
+
if (name.endsWith('=')) {
|
166
|
+
const name2 = name.substring(0, name.length - 1)
|
167
|
+
if (receiver === null)
|
168
|
+
throw `assignment to a global object ${name2} is not supported`
|
169
|
+
else if (args.length === 1) {
|
170
|
+
if (Reflect.set(receiver, name2, args[0]))
|
171
|
+
return args[0]
|
172
|
+
}
|
173
|
+
throw `faild to set an object property ${name2}`
|
174
|
+
}
|
175
|
+
|
176
|
+
if (receiver === null) {
|
177
|
+
const f = js_eval(name)
|
178
|
+
if (typeof f === 'function')
|
179
|
+
return f.apply(this, args)
|
180
|
+
else if (args.length === 0)
|
181
|
+
return f // obtain a property
|
182
|
+
}
|
183
|
+
else {
|
184
|
+
const f = Reflect.get(receiver, name)
|
185
|
+
if (f)
|
186
|
+
if (typeof f === 'function')
|
187
|
+
return Reflect.apply(f, receiver, args)
|
188
|
+
else if (args.length === 0)
|
189
|
+
return f // obtain a propety
|
190
|
+
}
|
191
|
+
|
192
|
+
throw `unknown function/method was called, ${name}`
|
193
|
+
}
|
194
|
+
|
195
|
+
let stdout_puts = console.log
|
196
|
+
|
197
|
+
const reply = value => {
|
198
|
+
if (value instanceof Promise)
|
199
|
+
value.then(result => { reply(result) })
|
200
|
+
.catch(err => reply_error(err))
|
201
|
+
else {
|
202
|
+
if (last_callback_stack_depth < callback_stack.length)
|
203
|
+
throw 'Ruby code was called without await. Call Jscall.close for recovery'
|
204
|
+
|
205
|
+
try {
|
206
|
+
const cmd = reply_with_piggyback([cmd_reply, encode_obj(value)])
|
207
|
+
const data = JSON.stringify(cmd)
|
208
|
+
stdout_puts(data)
|
209
|
+
} catch (e) {
|
210
|
+
reply_error(e)
|
211
|
+
}
|
212
|
+
}
|
213
|
+
}
|
214
|
+
|
215
|
+
const reply_error = e => {
|
216
|
+
const cmd = reply_with_piggyback([cmd_reply, encode_error(e)])
|
217
|
+
stdout_puts(JSON.stringify(cmd))
|
218
|
+
}
|
219
|
+
|
220
|
+
export const scavenge_references = () => {
|
221
|
+
reply_counter = 200
|
222
|
+
imported.kill_canary()
|
223
|
+
return true
|
224
|
+
}
|
225
|
+
|
226
|
+
const reply_with_piggyback = cmd => {
|
227
|
+
const threashold = 100
|
228
|
+
if (++reply_counter > threashold) {
|
229
|
+
reply_counter = 0
|
230
|
+
const dead_refs = imported.dead_references()
|
231
|
+
if (dead_refs.length > 0) {
|
232
|
+
const cmd2 = cmd.concat()
|
233
|
+
cmd2[4] = dead_refs
|
234
|
+
return cmd2
|
235
|
+
}
|
236
|
+
}
|
237
|
+
|
238
|
+
return cmd
|
239
|
+
}
|
240
|
+
|
241
|
+
const callback_stack = []
|
242
|
+
let last_callback_stack_depth = 0
|
243
|
+
let reply_counter = 0
|
244
|
+
|
245
|
+
export const exec = src => {
|
246
|
+
return new Promise((resolve, reject) => {
|
247
|
+
const cmd = reply_with_piggyback([cmd_eval, src])
|
248
|
+
callback_stack.push([resolve, reject])
|
249
|
+
stdout_puts(JSON.stringify(cmd))
|
250
|
+
})
|
251
|
+
}
|
252
|
+
|
253
|
+
const funcall_to_ruby = (receiver_id, name, args) => {
|
254
|
+
return new Promise((resolve, reject) => {
|
255
|
+
const receiver = [param_local_object, receiver_id]
|
256
|
+
const encoded_args = args.map(e => encode_obj(e))
|
257
|
+
const cmd = reply_with_piggyback([cmd_call, receiver, name, encoded_args])
|
258
|
+
callback_stack.push([resolve, reject])
|
259
|
+
stdout_puts(JSON.stringify(cmd))
|
260
|
+
})
|
261
|
+
}
|
262
|
+
|
263
|
+
const returned_from_callback = cmd => {
|
264
|
+
const result = decode_obj(cmd[1])
|
265
|
+
const callback = callback_stack.pop()
|
266
|
+
if (result instanceof RubyError)
|
267
|
+
callback[1](result.get())
|
268
|
+
else
|
269
|
+
callback[0](result)
|
270
|
+
}
|
271
|
+
|
272
|
+
class JsonReader {
|
273
|
+
constructor(stream) {
|
274
|
+
this.stream = stream
|
275
|
+
this.acc = ""
|
276
|
+
}
|
277
|
+
|
278
|
+
async *[Symbol.asyncIterator]() {
|
279
|
+
for await (let data of this.stream) {
|
280
|
+
this.acc += data
|
281
|
+
try {
|
282
|
+
yield JSON.parse(this.acc)
|
283
|
+
this.acc = ""
|
284
|
+
} catch {
|
285
|
+
// keep data in this.acc
|
286
|
+
}
|
287
|
+
}
|
288
|
+
if (this.acc != "") {
|
289
|
+
yield JSON.parse(this.acc)
|
290
|
+
}
|
291
|
+
}
|
292
|
+
}
|
293
|
+
|
294
|
+
export const start = async (stdin, use_stdout) => {
|
295
|
+
if (use_stdout)
|
296
|
+
console.log = console.error // on node.js
|
297
|
+
else
|
298
|
+
stdout_puts = (m) => stdin.puts(m) // on browser
|
299
|
+
|
300
|
+
stdin.setEncoding('utf8')
|
301
|
+
for await (const cmd of new JsonReader(stdin)) {
|
302
|
+
try {
|
303
|
+
last_callback_stack_depth = callback_stack.length
|
304
|
+
|
305
|
+
// scavenge remote references
|
306
|
+
if (cmd.length > 4)
|
307
|
+
cmd[4].forEach(i => exported.remove(i))
|
308
|
+
|
309
|
+
if (cmd[0] == cmd_eval) {
|
310
|
+
const result = js_eval(cmd[1])
|
311
|
+
reply(result)
|
312
|
+
}
|
313
|
+
else if (cmd[0] == cmd_call) {
|
314
|
+
const result = funcall_from_ruby(cmd)
|
315
|
+
reply(result)
|
316
|
+
}
|
317
|
+
else if (cmd[0] == cmd_reply)
|
318
|
+
returned_from_callback(cmd)
|
319
|
+
else
|
320
|
+
reply_error('invalid command')
|
321
|
+
} catch (error) {
|
322
|
+
const msg = typeof error === 'string' ? error : error.toString() +
|
323
|
+
'\n ---\n' + error.stack
|
324
|
+
reply_error(msg)
|
325
|
+
}
|
326
|
+
}
|
327
|
+
}
|
328
|
+
|
329
|
+
// for testing and debugging
|
330
|
+
export const get_exported_imported = () => {
|
331
|
+
return [exported, imported]
|
332
|
+
}
|
333
|
+
|
334
|
+
export const dyn_import = async (file_name, var_name) => {
|
335
|
+
const m = await import(file_name) // dynamic import
|
336
|
+
if (var_name)
|
337
|
+
eval(`(v)=>{globalThis.${var_name} = v}`)(m)
|
338
|
+
|
339
|
+
return m
|
340
|
+
}
|
data/lib/jscall.rb
ADDED
@@ -0,0 +1,344 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (C) 2022- Shigeru Chiba. All rights reserved.
|
4
|
+
|
5
|
+
require "json"
|
6
|
+
require "weakref"
|
7
|
+
|
8
|
+
require_relative "jscall/version"
|
9
|
+
require_relative "jscall/browser"
|
10
|
+
|
11
|
+
module Jscall
|
12
|
+
TableSize = 100
|
13
|
+
|
14
|
+
class JavaScriptError < RuntimeError
|
15
|
+
def initialize(msg)
|
16
|
+
super(msg)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class HiddenRef
|
21
|
+
def initialize(obj)
|
22
|
+
@object = obj
|
23
|
+
end
|
24
|
+
def __getobj__()
|
25
|
+
@object
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Exported # inbound referneces from Node to Ruby
|
30
|
+
attr_reader :objects
|
31
|
+
|
32
|
+
def initialize
|
33
|
+
ary = Array.new(Jscall::TableSize) {|i| i + 1}
|
34
|
+
ary[-1] = -1
|
35
|
+
@objects = HiddenRef.new(ary)
|
36
|
+
@free_element = 0
|
37
|
+
@hashtable = HiddenRef.new({})
|
38
|
+
end
|
39
|
+
|
40
|
+
def export(obj)
|
41
|
+
hash = @hashtable.__getobj__
|
42
|
+
if hash.include?(obj)
|
43
|
+
hash[obj]
|
44
|
+
else
|
45
|
+
objs = @objects.__getobj__
|
46
|
+
idx = @free_element
|
47
|
+
if idx < 0
|
48
|
+
idx = objs.size
|
49
|
+
else
|
50
|
+
@free_element = objs[idx]
|
51
|
+
end
|
52
|
+
objs[idx] = obj
|
53
|
+
hash[obj] = idx # return idx
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def find(idx)
|
58
|
+
obj = @objects.__getobj__[idx]
|
59
|
+
if obj.is_a?(Numeric)
|
60
|
+
raise JavaScriptError.new("unknown index is given to find(): #{idx}")
|
61
|
+
else
|
62
|
+
obj
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def remove(idx)
|
67
|
+
objects = @objects.__getobj__
|
68
|
+
obj = objects[idx]
|
69
|
+
if obj.is_a?(Numeric)
|
70
|
+
raise JavaScriptError.new("unknown index is given to remove(): #{idx}")
|
71
|
+
else
|
72
|
+
objects[idx] = @free_element
|
73
|
+
@free_element = idx
|
74
|
+
@hashtable.__getobj__.delete(obj)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class RemoteRef
|
80
|
+
attr_reader :id
|
81
|
+
|
82
|
+
def initialize(id)
|
83
|
+
@id = id
|
84
|
+
end
|
85
|
+
|
86
|
+
def method_missing(name, *args)
|
87
|
+
Jscall.__getpipe__.funcall(self, name, args)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
class Imported # outbound references from Ruby to Node
|
92
|
+
attr_reader :objects
|
93
|
+
|
94
|
+
def initialize
|
95
|
+
ary = Array.new(Jscall::TableSize, nil)
|
96
|
+
@objects = HiddenRef.new(ary)
|
97
|
+
@canary = WeakRef.new(RemoteRef.new(-1))
|
98
|
+
end
|
99
|
+
|
100
|
+
def import(index)
|
101
|
+
objects = @objects.__getobj__
|
102
|
+
wref = objects[index]
|
103
|
+
if wref&.weakref_alive?
|
104
|
+
wref.__getobj__
|
105
|
+
else
|
106
|
+
rref = RemoteRef.new(index)
|
107
|
+
objects[index] = WeakRef.new(rref)
|
108
|
+
rref
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# forces dead_references() to check the liveness of references.
|
113
|
+
def kill_canary
|
114
|
+
@canary = nil
|
115
|
+
end
|
116
|
+
|
117
|
+
def dead_references()
|
118
|
+
if @canary&.weakref_alive?
|
119
|
+
return []
|
120
|
+
else
|
121
|
+
@canary = WeakRef.new(RemoteRef.new(-1))
|
122
|
+
end
|
123
|
+
deads = []
|
124
|
+
objects = @objects.__getobj__
|
125
|
+
objects.each_index do |index|
|
126
|
+
wref = objects[index]
|
127
|
+
if !wref.nil? && !wref.weakref_alive?
|
128
|
+
objects[index] = nil
|
129
|
+
deads << index
|
130
|
+
end
|
131
|
+
end
|
132
|
+
deads
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
class PipeToJs
|
137
|
+
CMD_EVAL = 1
|
138
|
+
CMD_CALL = 2
|
139
|
+
CMD_REPLY = 3
|
140
|
+
Param_array = 0
|
141
|
+
Param_object = 1
|
142
|
+
Param_local_object = 2
|
143
|
+
Param_error = 3
|
144
|
+
|
145
|
+
@@node_cmd = 'node'
|
146
|
+
|
147
|
+
def self.node_command=(cmd)
|
148
|
+
@@node_cmd = cmd
|
149
|
+
end
|
150
|
+
|
151
|
+
# do import * as 'module_names[i][0]' from 'module_names[i][1]'
|
152
|
+
#
|
153
|
+
def initialize(module_names=[], options='')
|
154
|
+
startJS(module_names, options)
|
155
|
+
@exported = Exported.new
|
156
|
+
@imported = Imported.new
|
157
|
+
@send_counter = 0
|
158
|
+
end
|
159
|
+
|
160
|
+
# module_names: an array of [module_name, module_file_name]
|
161
|
+
#
|
162
|
+
def startJS(module_names, options)
|
163
|
+
script2 = ''
|
164
|
+
module_names.each_index do |i|
|
165
|
+
script2 += "import * as m#{i + 2} from \"#{module_names[i][1]}\"; globalThis.#{module_names[i][0]} = m#{i + 2}; "
|
166
|
+
end
|
167
|
+
script2 += "import { createRequire } from \"node:module\"; globalThis.require = createRequire(\"file://#{Dir.pwd}/\");"
|
168
|
+
script = "'import * as m1 from \"#{__dir__}/jscall/main.mjs\"; globalThis.Ruby = m1; #{script2}; Ruby.start(process.stdin, true)'"
|
169
|
+
@pipe = IO.popen("#{@@node_cmd} #{options} --input-type 'module' -e #{script}", "r+t")
|
170
|
+
@pipe.autoclose = true
|
171
|
+
end
|
172
|
+
|
173
|
+
def get_exported_imported
|
174
|
+
[@exported, @imported]
|
175
|
+
end
|
176
|
+
|
177
|
+
def close
|
178
|
+
@pipe.close
|
179
|
+
true
|
180
|
+
end
|
181
|
+
|
182
|
+
def encode_obj(obj)
|
183
|
+
if obj.is_a?(Numeric) || obj.is_a?(String) || obj.is_a?(Symbol) || obj == true || obj == false || obj == nil
|
184
|
+
obj
|
185
|
+
elsif obj.is_a?(Array)
|
186
|
+
[Param_array, obj.map {|e| encode_obj(e)}]
|
187
|
+
elsif obj.is_a?(RemoteRef)
|
188
|
+
[Param_local_object, obj.id]
|
189
|
+
else
|
190
|
+
[Param_object, @exported.export(obj)]
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def encode_error(msg)
|
195
|
+
[Param_error, msg]
|
196
|
+
end
|
197
|
+
|
198
|
+
def decode_obj(obj)
|
199
|
+
if obj.is_a?(Numeric) || obj.is_a?(String) || obj == true || obj == false || obj == nil
|
200
|
+
obj
|
201
|
+
elsif obj.is_a?(Array) && obj.size == 2
|
202
|
+
if (obj[0] == Param_array)
|
203
|
+
obj[1].map {|e| decode_obj(e)}
|
204
|
+
elsif (obj[0] == Param_object)
|
205
|
+
@imported.import(obj[1])
|
206
|
+
elsif (obj[0] == Param_local_object)
|
207
|
+
@exported.find(obj[1])
|
208
|
+
else # if Param_error
|
209
|
+
JavaScriptError.new(obj[1])
|
210
|
+
end
|
211
|
+
else
|
212
|
+
raise JavaScriptError.new('the result is a broken value')
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def funcall(receiver, name, args)
|
217
|
+
cmd = [CMD_CALL, encode_obj(receiver), name, args.map {|e| encode_obj(e)}]
|
218
|
+
send_command(cmd)
|
219
|
+
end
|
220
|
+
|
221
|
+
def exec(src)
|
222
|
+
cmd = [CMD_EVAL, src]
|
223
|
+
send_command(cmd)
|
224
|
+
end
|
225
|
+
|
226
|
+
def encode_eval_error(e)
|
227
|
+
traces = e.backtrace
|
228
|
+
location = if traces.size > 0 then traces[0] else '' end
|
229
|
+
encode_error(location + ' ' + e.to_s)
|
230
|
+
end
|
231
|
+
|
232
|
+
def scavenge
|
233
|
+
@send_counter = 200
|
234
|
+
@imported.kill_canary
|
235
|
+
exec 'Ruby.scavenge_references()'
|
236
|
+
end
|
237
|
+
|
238
|
+
def send_with_piggyback(cmd)
|
239
|
+
threashold = 100
|
240
|
+
@send_counter += 1
|
241
|
+
if (@send_counter > threashold)
|
242
|
+
@send_counter = 0
|
243
|
+
dead_refs = @imported.dead_references()
|
244
|
+
if (dead_refs.length > 0)
|
245
|
+
cmd2 = cmd.dup
|
246
|
+
cmd2[4] = dead_refs
|
247
|
+
return cmd2
|
248
|
+
end
|
249
|
+
end
|
250
|
+
return cmd
|
251
|
+
end
|
252
|
+
|
253
|
+
def send_command(cmd)
|
254
|
+
json_data = JSON.generate(send_with_piggyback(cmd))
|
255
|
+
@pipe.puts(json_data)
|
256
|
+
reply_data = @pipe.gets
|
257
|
+
reply = JSON.parse(reply_data || '[]')
|
258
|
+
if reply.length > 4
|
259
|
+
reply[4].each {|idx| @exported.remove(idx) }
|
260
|
+
end
|
261
|
+
if @pipe.closed?
|
262
|
+
raise RuntimeError.new("connection closed: #{reply}")
|
263
|
+
elsif reply[0] == CMD_REPLY
|
264
|
+
result = decode_obj(reply[1])
|
265
|
+
if result.is_a?(JavaScriptError)
|
266
|
+
raise result
|
267
|
+
else
|
268
|
+
return result
|
269
|
+
end
|
270
|
+
elsif reply[0] == CMD_EVAL
|
271
|
+
begin
|
272
|
+
result = Object::TOPLEVEL_BINDING.eval(reply[1])
|
273
|
+
encoded = encode_obj(result)
|
274
|
+
rescue => e
|
275
|
+
encoded = encode_eval_error(e)
|
276
|
+
end
|
277
|
+
send_command([CMD_REPLY, encoded])
|
278
|
+
elsif reply[0] == CMD_CALL
|
279
|
+
begin
|
280
|
+
receiver = decode_obj(reply[1])
|
281
|
+
name = reply[2]
|
282
|
+
args = reply[3].map {|e| decode_obj(e)}
|
283
|
+
result = receiver.public_send(name, *args)
|
284
|
+
encoded = encode_obj(result)
|
285
|
+
rescue => e
|
286
|
+
encoded = encode_eval_error(e)
|
287
|
+
end
|
288
|
+
send_command([CMD_REPLY, encoded])
|
289
|
+
else
|
290
|
+
raise RuntimeError.new("bad reply: #{reply}")
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
@pipe = nil
|
296
|
+
@module_names = []
|
297
|
+
@options = ''
|
298
|
+
@pipeToJsClass = PipeToJs
|
299
|
+
|
300
|
+
def self.config(module_names: [], options: '', browser: false)
|
301
|
+
@module_names = module_names
|
302
|
+
@options = options
|
303
|
+
@pipeToJsClass = if browser then PipeToBrowser else PipeToJs end
|
304
|
+
nil
|
305
|
+
end
|
306
|
+
|
307
|
+
def self.close
|
308
|
+
@pipe = nil if @pipe.close unless @pipe.nil?
|
309
|
+
end
|
310
|
+
|
311
|
+
Signal.trap(0) { self.close } # close before termination
|
312
|
+
|
313
|
+
def self.exec(src)
|
314
|
+
__getpipe__.exec(src)
|
315
|
+
end
|
316
|
+
|
317
|
+
def self.dyn_import(name, var_name=nil)
|
318
|
+
__getpipe__.funcall(nil, 'Ruby.dyn_import', [name, var_name])
|
319
|
+
end
|
320
|
+
|
321
|
+
# name is a string object.
|
322
|
+
# Evaluating this string in JavaScript results in a JavaScript function.
|
323
|
+
#
|
324
|
+
def self.funcall(name, *args)
|
325
|
+
__getpipe__.funcall(nil, name, args)
|
326
|
+
end
|
327
|
+
|
328
|
+
# reclaim unused remote references.
|
329
|
+
#
|
330
|
+
def self.scavenge_references
|
331
|
+
__getpipe__.scavenge
|
332
|
+
end
|
333
|
+
|
334
|
+
def self.method_missing(name, *args)
|
335
|
+
__getpipe__.funcall(nil, name, args)
|
336
|
+
end
|
337
|
+
|
338
|
+
def self.__getpipe__
|
339
|
+
if @pipe.nil?
|
340
|
+
@pipe = @pipeToJsClass.new(@module_names, @options)
|
341
|
+
end
|
342
|
+
@pipe
|
343
|
+
end
|
344
|
+
end
|
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jscall
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Shigeru Chiba
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-07-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: webrick
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.4'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.4'
|
27
|
+
description: a library for executing JavaScript code on node.js or a web browser
|
28
|
+
email:
|
29
|
+
- "?"
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- Gemfile
|
35
|
+
- LICENSE
|
36
|
+
- README.md
|
37
|
+
- Rakefile
|
38
|
+
- jscall.gemspec
|
39
|
+
- lib/jscall.rb
|
40
|
+
- lib/jscall/apple-touch-icon.png
|
41
|
+
- lib/jscall/browser.mjs
|
42
|
+
- lib/jscall/browser.rb
|
43
|
+
- lib/jscall/favicon.ico
|
44
|
+
- lib/jscall/jscall.html
|
45
|
+
- lib/jscall/main.mjs
|
46
|
+
- lib/jscall/version.rb
|
47
|
+
homepage: https://github.com/csg-tokyo/jscall
|
48
|
+
licenses: []
|
49
|
+
metadata:
|
50
|
+
homepage_uri: https://github.com/csg-tokyo/jscall
|
51
|
+
source_code_uri: https://github.com/csg-tokyo/jscall
|
52
|
+
post_install_message:
|
53
|
+
rdoc_options: []
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 2.6.0
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
requirements: []
|
67
|
+
rubygems_version: 3.1.6
|
68
|
+
signing_key:
|
69
|
+
specification_version: 4
|
70
|
+
summary: a library for calling JavaScript functions
|
71
|
+
test_files: []
|