js4j 0.1.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.
- package/README.md +684 -0
- package/examples/01-basic.js +75 -0
- package/examples/02-collections.js +108 -0
- package/examples/03-objects.js +99 -0
- package/examples/04-callbacks.js +97 -0
- package/examples/05-stdlib.js +123 -0
- package/examples/_gateway.js +78 -0
- package/index.d.ts +455 -0
- package/index.js +78 -0
- package/java/TestEntryPoint.java +262 -0
- package/java/py4j.jar +0 -0
- package/package.json +29 -0
- package/src/callbackServer.js +269 -0
- package/src/clientServer.js +81 -0
- package/src/collections.js +325 -0
- package/src/connection.js +253 -0
- package/src/exceptions.js +68 -0
- package/src/gateway.js +222 -0
- package/src/javaObject.js +267 -0
- package/src/jvmView.js +224 -0
- package/src/launcher.js +150 -0
- package/src/protocol.js +413 -0
- package/tests/comparison/run_comparison.js +279 -0
- package/tests/comparison/test_js4j.js +187 -0
- package/tests/comparison/test_py4j.py +171 -0
- package/tests/integration/gateway.integration.test.js +384 -0
- package/tests/unit/collections.test.js +313 -0
- package/tests/unit/exceptions.test.js +104 -0
- package/tests/unit/gateway.test.js +178 -0
- package/tests/unit/jvmView.test.js +126 -0
- package/tests/unit/launcher.test.js +27 -0
- package/tests/unit/protocol.test.js +280 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import java.util.*;
|
|
2
|
+
import py4j.GatewayServer;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* TestEntryPoint — the Java-side entry point object exposed to both py4j and js4j clients.
|
|
6
|
+
*
|
|
7
|
+
* All methods here are exercised by both the Python and JavaScript comparison tests.
|
|
8
|
+
* Results must be identical between the two client implementations.
|
|
9
|
+
*/
|
|
10
|
+
public class TestEntryPoint {
|
|
11
|
+
|
|
12
|
+
// ------------------------------------------------------------------
|
|
13
|
+
// Arithmetic
|
|
14
|
+
// ------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
public int add(int a, int b) {
|
|
17
|
+
return a + b;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public double addDoubles(double a, double b) {
|
|
21
|
+
return a + b;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public long addLongs(long a, long b) {
|
|
25
|
+
return a + b;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public int multiply(int a, int b) {
|
|
29
|
+
return a * b;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public double divide(double a, double b) {
|
|
33
|
+
return a / b;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ------------------------------------------------------------------
|
|
37
|
+
// Strings
|
|
38
|
+
// ------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
public String greet(String name) {
|
|
41
|
+
return "Hello, " + name + "!";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public String concatenate(String a, String b) {
|
|
45
|
+
return a + b;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public int stringLength(String s) {
|
|
49
|
+
return s.length();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public String toUpperCase(String s) {
|
|
53
|
+
return s.toUpperCase();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public boolean containsSubstring(String haystack, String needle) {
|
|
57
|
+
return haystack.contains(needle);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public String repeatString(String s, int times) {
|
|
61
|
+
StringBuilder sb = new StringBuilder();
|
|
62
|
+
for (int i = 0; i < times; i++) sb.append(s);
|
|
63
|
+
return sb.toString();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ------------------------------------------------------------------
|
|
67
|
+
// Booleans
|
|
68
|
+
// ------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
public boolean andBool(boolean a, boolean b) {
|
|
71
|
+
return a && b;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public boolean orBool(boolean a, boolean b) {
|
|
75
|
+
return a || b;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public boolean notBool(boolean a) {
|
|
79
|
+
return !a;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ------------------------------------------------------------------
|
|
83
|
+
// Null handling
|
|
84
|
+
// ------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
public String maybeNull(boolean returnNull) {
|
|
87
|
+
return returnNull ? null : "not null";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public boolean isNull(Object obj) {
|
|
91
|
+
return obj == null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ------------------------------------------------------------------
|
|
95
|
+
// Collections — returns wrapped Java collections
|
|
96
|
+
// ------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
public List<String> getStringList() {
|
|
99
|
+
List<String> list = new ArrayList<>();
|
|
100
|
+
list.add("alpha");
|
|
101
|
+
list.add("beta");
|
|
102
|
+
list.add("gamma");
|
|
103
|
+
return list;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public List<Integer> getIntList() {
|
|
107
|
+
List<Integer> list = new ArrayList<>();
|
|
108
|
+
list.add(1);
|
|
109
|
+
list.add(2);
|
|
110
|
+
list.add(3);
|
|
111
|
+
list.add(4);
|
|
112
|
+
list.add(5);
|
|
113
|
+
return list;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
public Set<String> getStringSet() {
|
|
117
|
+
Set<String> set = new LinkedHashSet<>();
|
|
118
|
+
set.add("one");
|
|
119
|
+
set.add("two");
|
|
120
|
+
set.add("three");
|
|
121
|
+
return set;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
public Map<String, Integer> getStringIntMap() {
|
|
125
|
+
Map<String, Integer> map = new LinkedHashMap<>();
|
|
126
|
+
map.put("a", 1);
|
|
127
|
+
map.put("b", 2);
|
|
128
|
+
map.put("c", 3);
|
|
129
|
+
return map;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public int sumList(List<Integer> list) {
|
|
133
|
+
int total = 0;
|
|
134
|
+
for (int v : list) total += v;
|
|
135
|
+
return total;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public int listSize(List<?> list) {
|
|
139
|
+
return list.size();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ------------------------------------------------------------------
|
|
143
|
+
// Arrays
|
|
144
|
+
// ------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
public int[] getIntArray() {
|
|
147
|
+
return new int[]{10, 20, 30, 40, 50};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
public String[] getStringArray() {
|
|
151
|
+
return new String[]{"x", "y", "z"};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
public int sumArray(int[] arr) {
|
|
155
|
+
int total = 0;
|
|
156
|
+
for (int v : arr) total += v;
|
|
157
|
+
return total;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ------------------------------------------------------------------
|
|
161
|
+
// Exceptions
|
|
162
|
+
// ------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
public void throwException(String message) {
|
|
165
|
+
throw new RuntimeException(message);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
public int divideInts(int a, int b) {
|
|
169
|
+
return a / b; // throws ArithmeticException on b=0
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ------------------------------------------------------------------
|
|
173
|
+
// Object creation (returns instances of inner classes)
|
|
174
|
+
// ------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
public Counter createCounter(int initial) {
|
|
177
|
+
return new Counter(initial);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
public static class Counter {
|
|
181
|
+
private int value;
|
|
182
|
+
|
|
183
|
+
public Counter(int initial) {
|
|
184
|
+
this.value = initial;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
public void increment() { value++; }
|
|
188
|
+
public void decrement() { value--; }
|
|
189
|
+
public void add(int n) { value += n; }
|
|
190
|
+
public int getValue() { return value; }
|
|
191
|
+
public void reset() { value = 0; }
|
|
192
|
+
|
|
193
|
+
@Override
|
|
194
|
+
public String toString() {
|
|
195
|
+
return "Counter(" + value + ")";
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ------------------------------------------------------------------
|
|
200
|
+
// StringBuilder (tests creating Java objects from the client side)
|
|
201
|
+
// ------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
public String buildString(String initial, String[] parts) {
|
|
204
|
+
StringBuilder sb = new StringBuilder(initial);
|
|
205
|
+
for (String p : parts) sb.append(p);
|
|
206
|
+
return sb.toString();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ------------------------------------------------------------------
|
|
210
|
+
// Type round-trips (verify encoding/decoding symmetry)
|
|
211
|
+
// ------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
public int echoInt(int v) { return v; }
|
|
214
|
+
public long echoLong(long v) { return v; }
|
|
215
|
+
public double echoDouble(double v) { return v; }
|
|
216
|
+
public boolean echoBool(boolean v) { return v; }
|
|
217
|
+
public String echoString(String v) { return v; }
|
|
218
|
+
public byte[] echoBytes(byte[] v) { return v; }
|
|
219
|
+
|
|
220
|
+
// ------------------------------------------------------------------
|
|
221
|
+
// Varargs
|
|
222
|
+
// ------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
public int sumVarargs(int... values) {
|
|
225
|
+
int total = 0;
|
|
226
|
+
for (int v : values) total += v;
|
|
227
|
+
return total;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
public String joinStrings(String sep, String... parts) {
|
|
231
|
+
return String.join(sep, parts);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ------------------------------------------------------------------
|
|
235
|
+
// Static access demo (via jvm namespace)
|
|
236
|
+
// ------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
// These are tested via gateway.jvm.java.lang.Math.abs(-5) etc.
|
|
239
|
+
// Nothing needed here; tests exercise the JVM directly.
|
|
240
|
+
|
|
241
|
+
// ------------------------------------------------------------------
|
|
242
|
+
// Main — starts the GatewayServer
|
|
243
|
+
// ------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
public static void main(String[] args) throws Exception {
|
|
246
|
+
int port = args.length > 0 ? Integer.parseInt(args[0]) : 25333;
|
|
247
|
+
TestEntryPoint entryPoint = new TestEntryPoint();
|
|
248
|
+
GatewayServer server = new GatewayServer(entryPoint, port);
|
|
249
|
+
server.start();
|
|
250
|
+
// Signal readiness to the test harness
|
|
251
|
+
System.out.println("GATEWAY_STARTED:" + server.getListeningPort());
|
|
252
|
+
System.out.flush();
|
|
253
|
+
|
|
254
|
+
// Keep alive until stdin is closed (test harness closes it on teardown)
|
|
255
|
+
try {
|
|
256
|
+
while (System.in.read() != -1) {}
|
|
257
|
+
} catch (Exception e) {
|
|
258
|
+
// ignore
|
|
259
|
+
}
|
|
260
|
+
server.shutdown();
|
|
261
|
+
}
|
|
262
|
+
}
|
package/java/py4j.jar
ADDED
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "js4j",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A Node.js implementation of py4j — bridge between JavaScript and Java via the Py4J gateway protocol",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "jest --testPathPattern='tests/(unit|integration)' --forceExit",
|
|
9
|
+
"test:unit": "jest --testPathPattern='tests/unit' --forceExit",
|
|
10
|
+
"test:integration": "jest --testPathPattern='tests/integration' --forceExit --runInBand",
|
|
11
|
+
"test:comparison": "node tests/comparison/run_comparison.js",
|
|
12
|
+
"test:all": "npm test && npm run test:comparison"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/theo-kim/js4j"
|
|
17
|
+
},
|
|
18
|
+
"keywords": ["java", "bridge", "py4j", "jvm", "gateway"],
|
|
19
|
+
"author": "Theodore Kim",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"dependencies": {},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"jest": "^29.0.0"
|
|
24
|
+
},
|
|
25
|
+
"jest": {
|
|
26
|
+
"testEnvironment": "node",
|
|
27
|
+
"testTimeout": 30000
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const net = require('net');
|
|
4
|
+
const { EventEmitter } = require('events');
|
|
5
|
+
const { Js4JNetworkError, Js4JError } = require('./exceptions');
|
|
6
|
+
const {
|
|
7
|
+
CALL_COMMAND_NAME,
|
|
8
|
+
SUCCESS,
|
|
9
|
+
ERROR,
|
|
10
|
+
END,
|
|
11
|
+
END_COMMAND_PART,
|
|
12
|
+
NULL_TYPE,
|
|
13
|
+
VOID_TYPE,
|
|
14
|
+
encodeCommandPart,
|
|
15
|
+
decodeReturnValue,
|
|
16
|
+
decodeTypedValue,
|
|
17
|
+
} = require('./protocol');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A pool of JS objects that are exposed to Java as "Python proxies".
|
|
21
|
+
* When a JS object is passed as an argument to a Java method, and that
|
|
22
|
+
* Java method expects a Java interface, we register the JS object here.
|
|
23
|
+
* Java can then call methods on it via the CallbackServer.
|
|
24
|
+
*
|
|
25
|
+
* Mirrors py4j's PythonProxyPool.
|
|
26
|
+
*/
|
|
27
|
+
class ProxyPool extends EventEmitter {
|
|
28
|
+
constructor() {
|
|
29
|
+
super();
|
|
30
|
+
this._pool = new Map();
|
|
31
|
+
this._counter = 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Register a JS proxy object and return its ID. */
|
|
35
|
+
register(obj) {
|
|
36
|
+
const id = 'p' + this._counter++;
|
|
37
|
+
this._pool.set(id, obj);
|
|
38
|
+
return id;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Look up a proxy by ID. */
|
|
42
|
+
get(id) {
|
|
43
|
+
return this._pool.get(id) || null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Remove a proxy from the pool. */
|
|
47
|
+
remove(id) {
|
|
48
|
+
this._pool.delete(id);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Check if a proxy is registered. */
|
|
52
|
+
has(id) {
|
|
53
|
+
return this._pool.has(id);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Handles a single callback connection from Java.
|
|
59
|
+
* Reads py4j-protocol commands and dispatches them to JS proxy objects.
|
|
60
|
+
*/
|
|
61
|
+
class CallbackConnection {
|
|
62
|
+
constructor(socket, proxyPool, gatewayClient) {
|
|
63
|
+
this._socket = socket;
|
|
64
|
+
this._proxyPool = proxyPool;
|
|
65
|
+
this._gatewayClient = gatewayClient;
|
|
66
|
+
this._buffer = '';
|
|
67
|
+
|
|
68
|
+
socket.setEncoding('utf8');
|
|
69
|
+
socket.setNoDelay(true);
|
|
70
|
+
|
|
71
|
+
socket.on('data', (chunk) => {
|
|
72
|
+
this._buffer += chunk;
|
|
73
|
+
this._processBuffer();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
socket.on('error', () => {});
|
|
77
|
+
socket.on('close', () => {});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
_processBuffer() {
|
|
81
|
+
// Commands are newline-delimited sequences ending with "e\n"
|
|
82
|
+
// Read lines until we have a full command
|
|
83
|
+
const lines = [];
|
|
84
|
+
let remaining = this._buffer;
|
|
85
|
+
|
|
86
|
+
while (true) {
|
|
87
|
+
const idx = remaining.indexOf('\n');
|
|
88
|
+
if (idx === -1) break;
|
|
89
|
+
const line = remaining.substring(0, idx);
|
|
90
|
+
remaining = remaining.substring(idx + 1);
|
|
91
|
+
lines.push(line);
|
|
92
|
+
if (line === END) {
|
|
93
|
+
this._buffer = remaining;
|
|
94
|
+
this._handleCommand(lines);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
this._buffer = remaining;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async _handleCommand(lines) {
|
|
102
|
+
if (lines.length === 0) return;
|
|
103
|
+
|
|
104
|
+
const commandType = lines[0];
|
|
105
|
+
|
|
106
|
+
if (commandType === 'c') {
|
|
107
|
+
// Call command: c, proxy_id, method_name, [args...], e
|
|
108
|
+
const proxyId = lines[1];
|
|
109
|
+
const methodName = lines[2];
|
|
110
|
+
const argLines = lines.slice(3, lines.length - 1); // exclude trailing 'e'
|
|
111
|
+
|
|
112
|
+
const proxy = this._proxyPool.get(proxyId);
|
|
113
|
+
if (!proxy) {
|
|
114
|
+
this._sendError(`No proxy found with id: ${proxyId}`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const method = proxy[methodName];
|
|
119
|
+
if (typeof method !== 'function') {
|
|
120
|
+
this._sendError(`Method '${methodName}' not found on proxy`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const args = this._decodeArgs(argLines);
|
|
126
|
+
const result = await method.apply(proxy, args);
|
|
127
|
+
this._sendSuccess(result);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
this._sendError(err.message || String(err));
|
|
130
|
+
}
|
|
131
|
+
} else if (commandType === 'g') {
|
|
132
|
+
// Garbage-collect a proxy
|
|
133
|
+
const proxyId = lines[1];
|
|
134
|
+
this._proxyPool.remove(proxyId);
|
|
135
|
+
this._sendSuccessVoid();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
_decodeArgs(argLines) {
|
|
140
|
+
const args = [];
|
|
141
|
+
for (const line of argLines) {
|
|
142
|
+
if (line.length === 0) continue;
|
|
143
|
+
const typePrefix = line[0];
|
|
144
|
+
const value = line.slice(1);
|
|
145
|
+
args.push(decodeTypedValue(typePrefix, value, this._gatewayClient));
|
|
146
|
+
}
|
|
147
|
+
return args;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
_sendSuccess(value) {
|
|
151
|
+
let response;
|
|
152
|
+
if (value === null || value === undefined) {
|
|
153
|
+
response = '!' + SUCCESS + VOID_TYPE + '\n';
|
|
154
|
+
} else {
|
|
155
|
+
try {
|
|
156
|
+
const encoded = encodeCommandPart(value, this._proxyPool);
|
|
157
|
+
// encodeCommandPart returns "type+value\n"; wrap in return+success code
|
|
158
|
+
response = '!' + SUCCESS + encoded;
|
|
159
|
+
} catch (_) {
|
|
160
|
+
response = '!' + SUCCESS + VOID_TYPE + '\n';
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
this._socket.write(response);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
_sendSuccessVoid() {
|
|
167
|
+
this._socket.write('!' + SUCCESS + VOID_TYPE + '\n');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
_sendError(message) {
|
|
171
|
+
this._socket.write('!' + ERROR + message + '\n');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* CallbackServer — listens for Java → JS callback connections.
|
|
177
|
+
*
|
|
178
|
+
* When a JS object is passed to Java as a proxy (implementing a Java
|
|
179
|
+
* interface), Java connects back here to invoke methods on that object.
|
|
180
|
+
*
|
|
181
|
+
* Mirrors py4j's CallbackServer.
|
|
182
|
+
*/
|
|
183
|
+
class CallbackServer extends EventEmitter {
|
|
184
|
+
/**
|
|
185
|
+
* @param {object} options
|
|
186
|
+
* @param {string} [options.host='127.0.0.1']
|
|
187
|
+
* @param {number} [options.port=25334]
|
|
188
|
+
* @param {ProxyPool} options.proxyPool
|
|
189
|
+
* @param {object} options.gatewayClient
|
|
190
|
+
*/
|
|
191
|
+
constructor(options = {}) {
|
|
192
|
+
super();
|
|
193
|
+
this.host = options.host || '127.0.0.1';
|
|
194
|
+
this.port = options.port || 25334;
|
|
195
|
+
this._proxyPool = options.proxyPool || new ProxyPool();
|
|
196
|
+
this._gatewayClient = options.gatewayClient;
|
|
197
|
+
this._server = null;
|
|
198
|
+
this._connections = new Set();
|
|
199
|
+
this.listening = false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Start listening for Java callback connections. */
|
|
203
|
+
start() {
|
|
204
|
+
return new Promise((resolve, reject) => {
|
|
205
|
+
this._server = net.createServer((socket) => {
|
|
206
|
+
const conn = new CallbackConnection(socket, this._proxyPool, this._gatewayClient);
|
|
207
|
+
this._connections.add(conn);
|
|
208
|
+
socket.on('close', () => this._connections.delete(conn));
|
|
209
|
+
this.emit('connection', conn);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
this._server.on('error', reject);
|
|
213
|
+
|
|
214
|
+
this._server.listen(this.port, this.host, () => {
|
|
215
|
+
this.listening = true;
|
|
216
|
+
this.port = this._server.address().port; // capture actual port (useful if port=0)
|
|
217
|
+
this.emit('listening');
|
|
218
|
+
resolve(this);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Stop the callback server and close all connections. */
|
|
224
|
+
stop() {
|
|
225
|
+
return new Promise((resolve) => {
|
|
226
|
+
for (const conn of this._connections) {
|
|
227
|
+
try { conn._socket.destroy(); } catch (_) {}
|
|
228
|
+
}
|
|
229
|
+
this._connections.clear();
|
|
230
|
+
|
|
231
|
+
if (this._server) {
|
|
232
|
+
this._server.close(() => {
|
|
233
|
+
this.listening = false;
|
|
234
|
+
resolve();
|
|
235
|
+
});
|
|
236
|
+
this._server = null;
|
|
237
|
+
} else {
|
|
238
|
+
resolve();
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
get proxyPool() {
|
|
244
|
+
return this._proxyPool;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Helper to create a JS object that can be passed to Java as a callback proxy.
|
|
250
|
+
*
|
|
251
|
+
* @param {string[]} interfaces - Java interface FQNs this object implements
|
|
252
|
+
* @param {object} impl - Object with method implementations
|
|
253
|
+
* @returns {object} A proxy-tagged object ready to pass to Java
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* const handler = createJavaProxy(
|
|
257
|
+
* ['java.lang.Runnable'],
|
|
258
|
+
* { run: () => console.log('Java called run!') }
|
|
259
|
+
* );
|
|
260
|
+
* await gateway.jvm.java.lang.Thread(handler).start();
|
|
261
|
+
*/
|
|
262
|
+
function createJavaProxy(interfaces, impl) {
|
|
263
|
+
return Object.assign({}, impl, {
|
|
264
|
+
_js4jProxy: true,
|
|
265
|
+
_interfaces: interfaces,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
module.exports = { CallbackServer, ProxyPool, CallbackConnection, createJavaProxy };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ClientServer — bidirectional, pinned-thread communication model.
|
|
5
|
+
*
|
|
6
|
+
* In py4j, ClientServer ensures that callbacks from Java arrive on the
|
|
7
|
+
* same thread that made the original call into Java, enabling recursive
|
|
8
|
+
* Java → Python → Java call chains without deadlocks.
|
|
9
|
+
*
|
|
10
|
+
* In Node.js there is only one thread and the event loop handles
|
|
11
|
+
* concurrency, so the pinning behaviour is not required. This module
|
|
12
|
+
* wraps JavaGateway + CallbackServer with the same API surface as
|
|
13
|
+
* py4j's ClientServer for compatibility.
|
|
14
|
+
*
|
|
15
|
+
* Mirrors py4j's ClientServer class.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const { JavaGateway, GatewayParameters, CallbackServerParameters } = require('./gateway');
|
|
19
|
+
const { createJavaProxy } = require('./callbackServer');
|
|
20
|
+
|
|
21
|
+
class ClientServer {
|
|
22
|
+
/**
|
|
23
|
+
* @param {GatewayParameters|object} [javaParameters]
|
|
24
|
+
* @param {CallbackServerParameters|object} [pythonParameters]
|
|
25
|
+
*/
|
|
26
|
+
constructor(javaParameters, pythonParameters) {
|
|
27
|
+
this._gateway = new JavaGateway(javaParameters, pythonParameters);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Connect to the Java gateway AND start the callback server.
|
|
32
|
+
* @returns {Promise<ClientServer>}
|
|
33
|
+
*/
|
|
34
|
+
async connect() {
|
|
35
|
+
await this._gateway.connect();
|
|
36
|
+
await this._gateway.startCallbackServer();
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Shut down the gateway and the callback server.
|
|
42
|
+
*/
|
|
43
|
+
async shutdown() {
|
|
44
|
+
await this._gateway.shutdown();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The jvm namespace (same as gateway.jvm). */
|
|
48
|
+
get jvm() {
|
|
49
|
+
return this._gateway.jvm;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The entry point object (same as gateway.entry_point). */
|
|
53
|
+
get entry_point() {
|
|
54
|
+
return this._gateway.entry_point;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The underlying JavaGateway. */
|
|
58
|
+
get gateway() {
|
|
59
|
+
return this._gateway;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The callback server. */
|
|
63
|
+
get callbackServer() {
|
|
64
|
+
return this._gateway._callbackServer;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Delegate convenience methods
|
|
68
|
+
getField(obj, name) { return this._gateway.getField(obj, name); }
|
|
69
|
+
setField(obj, name, val) { return this._gateway.setField(obj, name, val); }
|
|
70
|
+
newArray(cls, ...dims) { return this._gateway.newArray(cls, ...dims); }
|
|
71
|
+
newJvmView(name) { return this._gateway.newJvmView(name); }
|
|
72
|
+
getMethods(obj) { return this._gateway.getMethods(obj); }
|
|
73
|
+
getFields(obj) { return this._gateway.getFields(obj); }
|
|
74
|
+
getStaticMembers(cls) { return this._gateway.getStaticMembers(cls); }
|
|
75
|
+
javaImport(fqn) { return this._gateway.javaImport(fqn); }
|
|
76
|
+
help(target) { return this._gateway.help(target); }
|
|
77
|
+
releaseObject(obj) { return this._gateway.releaseObject(obj); }
|
|
78
|
+
detach(obj) { return this._gateway.detach(obj); }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { ClientServer, createJavaProxy };
|