forked from bastibe/transplant
-
Notifications
You must be signed in to change notification settings - Fork 0
/
transplant_remote.m
408 lines (388 loc) · 15.2 KB
/
transplant_remote.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
%TRANSPLANT is a Matlab server for remote code execution
% TRANSPLANT(URL) connects to a 0MQ client at a given URL.
%
% The client can send messages, which TRANSPLANT will answer.
% All messages are JSON-encoded strings. All message are structures
% with at least one key: 'type'
%
% Depending on the message type, other keys may or may not be set.
%
% TRANSPLANT implements the following message types:
% - 'die': closes the 0MQ session and quits Matlab.
% - 'set_global': saves the 'value' as a global variable called 'name'.
% - 'get_global': retrieve the value of a global variable 'name'.
% - 'set_proxy': saves the 'value' as a field called 'name' on cached
% object 'handle'.
% - 'get_proxy': retrieves the field called 'name' on cached object
% 'handle'.
% - 'del_proxy': remove cached object 'handle'.
% - 'call': call function 'name' with 'args' and 'nargout'.
%
% TRANSPLANT implements the following responses:
% - 'ack': received message successfully.
% - 'error': there was an error while handling the message.
% - 'value': returns a value.
%
% To enable cross-language functions, objects and matrices, these are
% encoded specially when transmitted between Python and Matlab:
% - Matrices are encoded as {"__matrix__", ... }
% - Functions are encoded as {"__function__", str2func(f) }
% - Objects are encoded as {"__object__", handle }
% (c) 2014 Bastian Bechtold
function transplant_remote(msgformat, url, is_zombie)
% this must be persistent to survive a SIGINT:
persistent proxied_objects is_receiving
% during restarts after a call to exit, normally safe things break,
% so just ignore them and continue exiting.
try
if nargin == 2
% normal startup
messenger = ZMQ(url);
proxied_objects = {};
is_receiving = false;
elseif nargin > 2 && is_zombie && ~is_receiving
% SIGINT has killed transplant_remote, but onCleanup has revived it
% At this point, neither lasterror nor MException.last is available,
% so we don't actually know where we were killed.
send_ack();
elseif nargin > 2 && is_zombie && is_receiving
% Sometimes, functions return normally, then trow a delayed error after
% they return. In that case, we crash within receive_msg. To recover,
% just continue receiving as if nothing had happened.
else
% no idea what happened. I don't want to live any more.
return
end
catch
return
end
% make sure that transplant doesn't crash on SIGINT
zombie = onCleanup(@()transplant_remote(msgformat, url, true));
while 1 % main messaging loop
try
is_receiving = true;
msg = receive_msg();
is_receiving = false;
msg = decode_values(msg);
switch msg('type')
case 'die' % exit matlab
send_ack();
quit('force');
case 'set_global' % save msg.value as a global variable
assignin('base', msg('name'), msg('value'));
send_ack();
case 'get_global' % retrieve the value of a global variable
% simply evalin('base', msg.name) would call functions,
% so that can't be used.
existance = evalin('base', ['exist(''' msg('name') ''')']);
% value does not exist:
if existance == 0
error('TRANSPLANT:novariable' , ...
['Undefined variable ''' msg('name') '''.']);
% value is a function:
elseif any(existance == [2, 3, 5, 6])
value = str2func(msg('name'));
else
value = evalin('base', msg('name'));
end
send_value(value);
case 'set_proxy' % set field value of a cached object
obj = proxied_objects{msg('handle')};
set(obj, msg.name, msg('value'));
send_ack();
case 'get_proxy' % retrieve field value of a cached object
obj = proxied_objects{msg('handle')};
value = get(obj, msg('name'));
send_value(value);
case 'del_proxy' % invalidate cached object
proxied_objects{msg('handle')} = [];
send_ack();
case 'call' % call a function
fun = str2func(msg('name'));
% get the number of output arguments
if isKey(msg, 'nargout') && msg('nargout') >= 0
resultsize = msg('nargout');
else
resultsize = nargout(fun);
end
if resultsize > 0
% call the function with the given number of
% output arguments:
results = cell(resultsize, 1);
args = msg('args');
[results{:}] = fun(args{:});
if length(results) == 1
send_value(results{1});
else
send_value(results);
end
else
% try to get output from ans:
clear('ans');
args = msg('args');
fun(args{:});
try
send_value(ans);
catch err
send_ack();
end
end
end
catch err
send_error(err)
end
end
% Send a message
%
% This is the base function for the specialized senders below
function send_message(message_type, message)
message('type') = message_type;
if strcmp(msgformat, 'msgpack')
messenger.send(dumpmsgpack(message));
else
str = dumpjson(message);
messenger.send(unicode2native(str, 'utf-8'));
end
end
% Send an acknowledgement message
function send_ack()
send_message('ack', containers.Map());
end
% Send an error message
function send_error(err)
message = containers.Map();
message('identifier') = err.identifier;
message('message') = err.message;
% ignore stack frames from the last restart
is_cleanup = @(s)~isempty(strfind(s.name, 'onCleanup.delete'));
cleanup_idx = find(arrayfun(is_cleanup, err.stack), 1);
if ~isempty(cleanup_idx)
message('stack') = err.stack([1:cleanup_idx-2]);
else
message('stack') = err.stack;
end
send_message('error', message);
end
% Send a message that contains a value
function send_value(value)
message = containers.Map();
message('value') = encode_values(value);
send_message('value', message);
end
% Wait for and receive a message
function message = receive_msg()
blob = messenger.receive();
if strcmp(msgformat, 'msgpack')
message = decode_values(parsemsgpack(blob));
else
str = native2unicode(blob, 'utf-8');
message = decode_values(parsejson(str));
end
end
% recursively step through value and encode all occurrences of
% matrices, objects and functions as special cell arrays.
function [value] = encode_values(value)
if issparse(value)
value = encode_sparse_matrix(value);
elseif (isnumeric(value) && numel(value) ~= 0 && ...
(numel(value) > 1 || ~isreal(value)))
value = encode_matrix(value);
elseif isa(value, 'containers.Map')
% containers.Map is a handle object, so we need to create a
% new copy her in order to not change the original object.
out = containers.Map();
for key=value.keys()
out(key{1}) = encode_values(value(key{1}));
end
value = out;
elseif isobject(value)
value = encode_object(value);
elseif isa(value, 'function_handle')
value = {'__function__', func2str(value)};
elseif iscell(value)
for idx=1:numel(value)
value{idx} = encode_values(value{idx});
end
elseif isstruct(value)
keys = fieldnames(value);
for idx=1:numel(value)
for n=1:length(keys)
key = keys{n};
value(idx).(key) = encode_values(value(idx).(key));
end
end
end
end
% recursively step through value and decode all special cell arrays
% that contain matrices, objects, or functions.
function [value] = decode_values(value)
if iscell(value)
len = numel(value);
special = len > 0 && ischar(value{1});
if special && len == 4 && strcmp(value{1}, '__matrix__')
value = decode_matrix(value);
elseif special && len == 5 && strcmp(value{1}, '__sparse__')
value = decode_sparse_matrix(value);
elseif special && len == 2 && strcmp(value{1}, '__object__')
value = proxied_objects{value{2}};
elseif special && len == 2 && strcmp(value{1}, '__function__')
value = str2func(value{2});
else
for idx=1:numel(value)
value{idx} = decode_values(value{idx});
end
end
elseif isa(value, 'containers.Map')
for key=value.keys()
value(key{1}) = decode_values(value(key{1}));
end
elseif isstruct(value)
keys = fieldnames(value);
for idx=1:numel(value)
for n=1:length(keys)
key = keys{n};
value(idx).(key) = decode_values(value(idx).(key));
end
end
end
end
% Objects are cached, and they are encoded as special cell arrays
% `{"__object__", cache_index}`
function [out] = encode_object(object)
if length(object) > 1
out = {};
for n=1:length(object)
out{n} = encode_object(object(n));
end
else
first_empty_entry = find(cellfun('isempty', proxied_objects), 1);
if isempty(first_empty_entry)
first_empty_entry = length(proxied_objects)+1;
end
proxied_objects{first_empty_entry} = object;
out = {'__object__', first_empty_entry};
end
end
end
% The matrix `int32([1 2; 3 4])` would be encoded as
% `{'__matrix__', 'int32', [2, 2], 'AQAAAAIAAAADAAAABAAAA==\n'}`
%
% where `'int32'` is the data type, `[2, 2]` is the matrix shape and
% `'AQAAAAIAAAADAAAABAAAA==\n"'` is the base64-encoded matrix content.
function [value] = encode_matrix(value)
if ~isreal(value) && isinteger(value)
value = double(value); % Numpy does not know complex int
end
% convert column-major (Matlab, FORTRAN) to row-major (C, Python)
value = permute(value, length(size(value)):-1:1);
% convert to uint8 1-D array
if isreal(value)
binary = typecast(value(:), 'uint8');
else
% convert [complex, complex] into [real, imag, real, imag]
tmp = zeros(numel(value)*2, 1);
if isa(value, 'single')
tmp = single(tmp);
end
tmp(1:2:end) = real(value(:));
tmp(2:2:end) = imag(value(:));
binary = typecast(tmp, 'uint8');
end
if islogical(value)
% convert logicals (bool) into one-byte-per-bit
binary = cast(value, 'uint8');
end
% not all typecasts return column vectors, so use (:)
base64 = base64encode(binary(:));
% translate Matlab class names into numpy dtypes
if isa(value, 'double') && isreal(value)
dtype = 'float64';
elseif isa(value, 'double')
dtype = 'complex128';
elseif isa(value, 'single') && isreal(value)
dtype = 'float32';
elseif isa(value, 'single')
dtype = 'complex64';
elseif isa(value, 'logical')
dtype = 'bool';
elseif isinteger(value)
dtype = class(value);
else
return % don't encode
end
% save as row-major (C, Python)
value = {'__matrix__', dtype, fliplr(size(value)), base64};
end
% The matrix `int32([1 2; 3 4])` would be encoded as
% `{'__matrix__', 'int32', [2, 2], 'AQAAAAIAAAADAAAABAAAA==\n'}`
%
% where `'int32'` is the data type, `[2, 2]` is the matrix shape and
% `'AQAAAAIAAAADAAAABAAAA==\n'` is the base64-encoded matrix content.
function [value] = decode_matrix(value)
dtype = value{2};
shape = cell2mat(value{3});
if length(shape) == 0
shape = [1 1];
elseif length(shape) == 1
shape = [1 shape];
end
binary = base64decode(value{4});
% translate numpy dtypes into Matlab class names
if strcmp(dtype, 'complex128')
value = typecast(binary, 'double')';
value = value(1:2:end) + 1i*value(2:2:end);
elseif strcmp(dtype, 'float64')
value = typecast(binary, 'double')';
elseif strcmp(dtype, 'complex64')
value = typecast(binary, 'single')';
value = value(1:2:end) + 1i*value(2:2:end);
elseif strcmp(dtype, 'float32')
value = typecast(binary, 'single')';
elseif strcmp(dtype, 'bool')
value = logical(binary);
else
value = typecast(binary, dtype);
end
% convert row-major (C, Python) to column-major (Matlab, FORTRAN)
value = reshape(value, fliplr(shape));
value = permute(value, length(shape):-1:1);
end
% Encode a sparse matrix as a special list.
% A sparse matrix `[[2, 0], [0, 3]]` would be encoded as
% `["__sparse__", [2, 2],
% <matrix for row indices [0, 1]>,
% <matrix for row indices [1, 0]>,
% <matrix for values [2, 3]>]`,
% where each `<matrix>` is encoded according `encode_matrix` and `[2,
% 2]` is the data shape.
function [value] = encode_sparse_matrix(value)
[row, col, data] = find(value);
if numel(data) > 0
value = {'__sparse__', fliplr(size(value)), ...
encode_matrix(row-1), encode_matrix(col-1), ...
encode_matrix(data)};
else
% don't try to encode empty matrices as matrices
value = {'__sparse__', fliplr(size(value)), [], [], []};
end
end
% Decode a special list to a sparse matrix.
% A sparse matrix
% `["__sparse__", [2, 2],
% <matrix for row indices [0, 1]>,
% <matrix for row indices [1, 0]>,
% <matrix for values [2, 3]>]`,
% where each `<matrix>` is encoded according `encode_matrix` would be
% decoded as `[[2, 0], [0, 3]]`.
function [value] = decode_sparse_matrix(value)
shape = double(cell2mat(value{2}));
if length(shape) == 0
shape = [1 1];
elseif length(shape) == 1
shape = [1 shape];
end
row = double(decode_matrix(value{3}));
col = double(decode_matrix(value{4}));
data = double(decode_matrix(value{5}));
value = sparse(row+1, col+1, data, shape(1), shape(2));
end