Skip to content

Commit abc7408

Browse files
committed
feat(swingset): support raw devices
"Raw devices" bypass the deviceSlots layer and allow device code direct access to `syscall`, and the arguments arriving through the `dispatch` object it must produce. This makes some patterns much easier to implement, such as producing new device nodes as part of the device's API (e.g. one device node per code bundle). It also provides vatstoreGet/Set/Delete, so the device code can manage one piece of state at a time, instead of doing an expensive read-modify-write cycle on a single large aggregate state object. A helper library named deviceTools.js was added to make it slightly easier to write a raw device. In the longer run (see #1346), we'd like these devices to support Promises and plain object references. This change doesn't go that far. The remaining limitations are: * the deviceTools.js library refuses to handle exported objects, imported foreign device nodes, or promises of any sort * the outbound translator (deviceKeeper.js `mapDeviceSlotToKernelSlot`) refuses to handle exported objects and exported promises * the vat outbound translator (vatTranslator.js `translateCallNow`) refuses to handle promises * liveslots rejects promises in `D()` arguments refs #1346
1 parent 42cb11e commit abc7408

File tree

7 files changed

+483
-72
lines changed

7 files changed

+483
-72
lines changed

packages/SwingSet/docs/devices.md

+45
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,51 @@ However the inbound message pathway uses `dispatch.invoke(deviceNodeID,
255255
method, argsCapdata) -> resultCapdata`, and the outbound pathway uses
256256
`syscall.sendOnly`.
257257
258+
## Raw Devices
259+
260+
An alternate way to write a device is to use the "raw device API". In this
261+
mode, there is no deviceSlots layer, and no attempt to provide
262+
object-capability abstractions. Instead, the device code is given a `syscall`
263+
object, and is expected to provide a `dispatch` object, and everything else
264+
is left up to the device.
265+
266+
This mode makes it possible to create new device nodes as part of the normal
267+
API, because the code can learn the raw device ref (dref) of the target
268+
device node on each inbound invocation, without needing a pre-registered
269+
table of JavaScript `Object` instances for every export.
270+
271+
Raw devices have access to a per-device string/string key-value store whose
272+
API matches the `vatStore` available to vats:
273+
274+
* `syscall.vatstoreGet(key)` -> `string`
275+
* `syscall.vatstoreSet(key, value)`
276+
* `syscall.vatstoreDelete(key)`
277+
278+
The mode is enabled by exporting a function named `buildDevice` instead of
279+
`buildRootDeviceNode`.
280+
281+
```js
282+
export function buildDevice(tools, endowments) {
283+
const { syscall } = tools;
284+
const dispatch = {
285+
invoke: (dnid, method, argsCapdata) => {
286+
..
287+
},
288+
};
289+
return dispatch;
290+
}
291+
```
292+
293+
To make it easier to write a raw device, a helper library named "deviceTools"
294+
is available in `src/deviceTools.js`. This provides a marshalling layer that
295+
can parse the incoming `argsCapdata` into representations of different sorts
296+
of objects, and a reverse direction for serializing the returned results.
297+
Unlike liveslots and deviceslots, this library makes no attempt to present
298+
the parsed output as invokable objects. When it parses `o-4` into a
299+
"Presence", you cannot use `E()` on that presence. However, you can extract
300+
the `o-4` from it. The library is most useful for building the data structure
301+
of the return results without manual JSON hacking.
302+
258303
## Kernel Devices
259304
260305
The kernel automatically configures devices for internal use. Most are paired

packages/SwingSet/src/deviceTools.js

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { assert, details as X } from '@agoric/assert';
2+
import { makeMarshal, Far } from '@agoric/marshal';
3+
import { parseVatSlot } from './parseVatSlots.js';
4+
5+
// raw devices can use this to build a set of convenience tools for
6+
// serialization/unserialization
7+
8+
export function buildSerializationTools(syscall, deviceName) {
9+
// TODO: prevent our Presence/DeviceNode objects from being accidentally be
10+
// marshal-serialized into persistent state
11+
12+
const presences = new WeakMap();
13+
const myDeviceNodes = new WeakMap();
14+
15+
function slotFromPresence(p) {
16+
return presences.get(p);
17+
}
18+
function presenceForSlot(slot) {
19+
const { type, allocatedByVat } = parseVatSlot(slot);
20+
assert.equal(type, 'object');
21+
assert.equal(allocatedByVat, false);
22+
const p = Far('presence', {
23+
send(method, args) {
24+
assert.typeof(method, 'string');
25+
assert(Array.isArray(args), args);
26+
// eslint-disable-next-line no-use-before-define
27+
const capdata = serialize(args);
28+
syscall.sendOnly(slot, method, capdata);
29+
},
30+
});
31+
presences.set(p, slot);
32+
return p;
33+
}
34+
35+
function slotFromMyDeviceNode(dn) {
36+
return myDeviceNodes.get(dn);
37+
}
38+
function deviceNodeForSlot(slot) {
39+
const { type, allocatedByVat } = parseVatSlot(slot);
40+
assert.equal(type, 'device');
41+
assert.equal(allocatedByVat, true);
42+
const dn = Far('device node', {});
43+
myDeviceNodes.set(dn, slot);
44+
return dn;
45+
}
46+
47+
function convertSlotToVal(slot) {
48+
const { type, allocatedByVat } = parseVatSlot(slot);
49+
if (type === 'object') {
50+
assert(!allocatedByVat, X`devices cannot yet allocate objects ${slot}`);
51+
return presenceForSlot(slot);
52+
} else if (type === 'device') {
53+
assert(
54+
allocatedByVat,
55+
X`devices should yet not be given other devices '${slot}'`,
56+
);
57+
return deviceNodeForSlot(slot);
58+
} else if (type === 'promise') {
59+
assert.fail(X`devices should not yet be given promises '${slot}'`);
60+
} else {
61+
assert.fail(X`unrecognized slot type '${type}'`);
62+
}
63+
}
64+
65+
function convertValToSlot(val) {
66+
const objSlot = slotFromPresence(val);
67+
if (objSlot) {
68+
return objSlot;
69+
}
70+
const devnodeSlot = slotFromMyDeviceNode(val);
71+
if (devnodeSlot) {
72+
return devnodeSlot;
73+
}
74+
throw Error(X`unable to convert value ${val}`);
75+
}
76+
77+
const m = makeMarshal(convertValToSlot, convertSlotToVal, {
78+
marshalName: `device:${deviceName}`,
79+
// TODO Temporary hack.
80+
// See https://github.com/Agoric/agoric-sdk/issues/2780
81+
errorIdNum: 60000,
82+
});
83+
84+
// for invoke(), these will unserialize the arguments, and serialize the
85+
// response (into a vatresult with the 'ok' header)
86+
const unserialize = capdata => m.unserialize(capdata);
87+
const serialize = data => m.serialize(harden(data));
88+
const returnFromInvoke = args => harden(['ok', serialize(args)]);
89+
90+
const tools = {
91+
slotFromPresence,
92+
presenceForSlot,
93+
slotFromMyDeviceNode,
94+
deviceNodeForSlot,
95+
unserialize,
96+
returnFromInvoke,
97+
};
98+
99+
return harden(tools);
100+
}
101+
harden(buildSerializationTools);

packages/SwingSet/src/kernel/deviceManager.js

+39-18
Original file line numberDiff line numberDiff line change
@@ -19,42 +19,64 @@ import '../types.js';
1919
* Produce an object that will serve as the kernel's handle onto a device.
2020
*
2121
* @param {string} deviceName The device's name, for human readable diagnostics
22-
* @param {*} buildRootDeviceNode
22+
* @param {*} deviceNamespace The module namespace object exported by the device bundle
2323
* @param {*} state A get/set object for the device's persistent state
2424
* @param {Record<string, any>} endowments The device's configured endowments
2525
* @param {*} testLog
2626
* @param {*} deviceParameters Parameters from the device's config entry
27+
* @param {*} deviceSyscallHandler
2728
*/
2829
export default function makeDeviceManager(
2930
deviceName,
30-
buildRootDeviceNode,
31+
deviceNamespace,
3132
state,
3233
endowments,
3334
testLog,
3435
deviceParameters,
36+
deviceSyscallHandler,
3537
) {
36-
let deviceSyscallHandler;
37-
function setDeviceSyscallHandler(handler) {
38-
deviceSyscallHandler = handler;
39-
}
40-
4138
const syscall = harden({
4239
sendOnly: (target, method, args) => {
4340
const dso = harden(['sendOnly', target, method, args]);
4441
deviceSyscallHandler(dso);
4542
},
43+
vatstoreGet: key => {
44+
const dso = harden(['vatstoreGet', key]);
45+
return deviceSyscallHandler(dso);
46+
},
47+
vatstoreSet: (key, value) => {
48+
const dso = harden(['vatstoreSet', key, value]);
49+
deviceSyscallHandler(dso);
50+
},
51+
vatstoreDelete: key => {
52+
const dso = harden(['vatstoreDelete', key]);
53+
deviceSyscallHandler(dso);
54+
},
4655
});
4756

48-
// Setting up the device runtime gives us back the device's dispatch object
49-
const dispatch = makeDeviceSlots(
50-
syscall,
51-
state,
52-
buildRootDeviceNode,
53-
deviceName,
54-
endowments,
55-
testLog,
56-
deviceParameters,
57-
);
57+
let dispatch;
58+
if (typeof deviceNamespace.buildDevice === 'function') {
59+
// raw device
60+
const tools = { syscall };
61+
// maybe add state utilities
62+
dispatch = deviceNamespace.buildDevice(tools, endowments);
63+
} else {
64+
assert(
65+
typeof deviceNamespace.buildRootDeviceNode === 'function',
66+
`device ${deviceName} lacks buildRootDeviceNode`,
67+
);
68+
69+
// Setting up the device runtime gives us back the device's dispatch object
70+
dispatch = makeDeviceSlots(
71+
syscall,
72+
state,
73+
deviceNamespace.buildRootDeviceNode,
74+
deviceName,
75+
endowments,
76+
testLog,
77+
deviceParameters,
78+
);
79+
}
5880

5981
/**
6082
* Invoke a method on a device node.
@@ -84,7 +106,6 @@ export default function makeDeviceManager(
84106

85107
const manager = {
86108
invoke,
87-
setDeviceSyscallHandler,
88109
};
89110
return manager;
90111
}

packages/SwingSet/src/kernel/kernel.js

+31-54
Original file line numberDiff line numberDiff line change
@@ -1019,52 +1019,6 @@ export default function buildKernel(
10191019
return vatID;
10201020
}
10211021

1022-
function buildDeviceManager(
1023-
deviceID,
1024-
name,
1025-
buildRootDeviceNode,
1026-
endowments,
1027-
deviceParameters,
1028-
) {
1029-
const deviceKeeper = kernelKeeper.allocateDeviceKeeperIfNeeded(deviceID);
1030-
// Wrapper for state, to give to the device to access its state.
1031-
// Devices are allowed to get their state at startup, and set it anytime.
1032-
// They do not use orthogonal persistence or transcripts.
1033-
const state = harden({
1034-
get() {
1035-
return deviceKeeper.getDeviceState();
1036-
},
1037-
set(value) {
1038-
deviceKeeper.setDeviceState(value);
1039-
},
1040-
});
1041-
const manager = makeDeviceManager(
1042-
name,
1043-
buildRootDeviceNode,
1044-
state,
1045-
endowments,
1046-
testLog,
1047-
deviceParameters,
1048-
);
1049-
return manager;
1050-
}
1051-
1052-
// plug a new DeviceManager into the kernel
1053-
function addDeviceManager(deviceID, name, manager) {
1054-
const translators = makeDeviceTranslators(deviceID, name, kernelKeeper);
1055-
function deviceSyscallHandler(deviceSyscallObject) {
1056-
const ksc = translators.deviceSyscallToKernelSyscall(deviceSyscallObject);
1057-
// devices can only do syscall.sendOnly, which has no results
1058-
kernelSyscallHandler.doKernelSyscall(ksc);
1059-
}
1060-
manager.setDeviceSyscallHandler(deviceSyscallHandler);
1061-
1062-
ephemeral.devices.set(deviceID, {
1063-
translators,
1064-
manager,
1065-
});
1066-
}
1067-
10681022
async function start() {
10691023
if (started) {
10701024
throw Error('kernel.start already called');
@@ -1102,24 +1056,47 @@ export default function buildKernel(
11021056
assertKnownOptions(options, ['deviceParameters', 'unendowed']);
11031057
const { deviceParameters = {}, unendowed } = options;
11041058
const devConsole = makeConsole(`${debugPrefix}SwingSet:dev-${name}`);
1059+
11051060
// eslint-disable-next-line no-await-in-loop
11061061
const NS = await importBundle(source.bundle, {
11071062
filePrefix: `dev-${name}/...`,
11081063
endowments: harden({ ...vatEndowments, console: devConsole, assert }),
11091064
});
1110-
assert(
1111-
typeof NS.buildRootDeviceNode === 'function',
1112-
`device ${name} lacks buildRootDeviceNode`,
1113-
);
1065+
11141066
if (deviceEndowments[name] || unendowed) {
1115-
const manager = buildDeviceManager(
1116-
deviceID,
1067+
const translators = makeDeviceTranslators(deviceID, name, kernelKeeper);
1068+
function deviceSyscallHandler(deviceSyscallObject) {
1069+
const ksc = translators.deviceSyscallToKernelSyscall(
1070+
deviceSyscallObject,
1071+
);
1072+
const kres = kernelSyscallHandler.doKernelSyscall(ksc);
1073+
const dres = translators.kernelResultToDeviceResult(ksc[0], kres);
1074+
assert.equal(dres[0], 'ok');
1075+
return dres[1];
1076+
}
1077+
1078+
// Wrapper for state, to give to the device to access its state.
1079+
// Devices are allowed to get their state at startup, and set it anytime.
1080+
// They do not use orthogonal persistence or transcripts.
1081+
const state = harden({
1082+
get() {
1083+
return deviceKeeper.getDeviceState();
1084+
},
1085+
set(value) {
1086+
deviceKeeper.setDeviceState(value);
1087+
},
1088+
});
1089+
1090+
const manager = makeDeviceManager(
11171091
name,
1118-
NS.buildRootDeviceNode,
1092+
NS,
1093+
state,
11191094
deviceEndowments[name],
1095+
testLog,
11201096
deviceParameters,
1097+
deviceSyscallHandler,
11211098
);
1122-
addDeviceManager(deviceID, name, manager);
1099+
ephemeral.devices.set(deviceID, { translators, manager });
11231100
} else {
11241101
console.log(
11251102
`WARNING: skipping device ${deviceID} (${name}) because it has no endowments`,

0 commit comments

Comments
 (0)