jscall 1.0.0
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 +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: []
|