Skip to content

Instantly share code, notes, and snippets.

@jfparadis
Last active January 24, 2020 00:03
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jfparadis/cfd2e965047131fcfac1cb81fdb9b863 to your computer and use it in GitHub Desktop.
Save jfparadis/cfd2e965047131fcfac1cb81fdb9b863 to your computer and use it in GitHub Desktop.
XS Compartment shim

XS Compartment Shim

A Compartment object abstracts the notion of a distinct global environment, with its own global object, copy of the standard library, but shares the "intrinsics" (standard objects that are not bound to global variables, like the initial value of Object.prototype) with the current execution environment.

Compartment Constructor

class Compartment {
  constructor: (
    globals : object?,                  // extra globals added to the global object
    modules: object?,                   // module map, specifier to specifier
  ) -> object,

  // public methods
  import(specifier: string) -> promise, // same argument/return value as dynamic import

  // inherited accessor properties
  get global() -> object,               // access this compartment global object
}

Implementation

  • Overrides the native Compartment API. The native one isn't available anymore (except to the shim itself).
  • Uses a 3-level module map to go from specifier to specifier withought resorting to symbols. The original Compartment.map isn't available or necessary.

Shim Limitations

Only bare specifiers are supported (mostly an XS limitation). A clever mapping mechanism need to be developed.

Example

This is an port of the "variation" compartment example from XS: https://github.com/Moddable-OpenSource/moddable/tree/public/examples/js/compartments/variations

The changes are:

  • The example files are moved to a sandbox directory.
  • The top level 'main.jsloads thesandbox/main.js` and passes it a modules map.
  • The native XS Compartment API is not available anywhere except to the Compartment module.
  • the sandbox/main uses the new API.

The API has changed as folllow:

// XS
let { decrement, mod } = Compartment.map;
let mod1 = new Compartment("mod", { name:"mod1" }, { mod, vary:decrement });
mod1.export.test();
// Shim
const cmp1 = new Compartment({ name: 'mod1' }, { '*': { 'mod': 'mod', 'vary': 'decrement' } });
const mod1 = await cmp1.import('mod');
mod1.test();

Running

Place all files in a moddable examples/js/compartments/variation-2 folder.

manifest.json
main.js
Compartment.js 
sandbox/
  main.js
  mod.js
  decrement.js   
  increment.js             

Then open XSBug, cd into the directory from terminal, and run mcconfig -m -d at the command-line.

Output

Same output as the original XS example, plus a few debugging items.

Compartment imported
Compartment.map: Resource,instrumentation,screen,time,timer,Compartment,main,sandbox/main,sandbox/mod,sandbox/increment,sandbox/decrement,mc/config
main imported
Compartment imported
Compartment.map: Compartment,main,mod,increment,decrement
Importing main
sandbox/main imported
Compartment imported
Compartment.map: Compartment,mod,vary
Compartment imported
Compartment.map: Compartment,mod,vary
Importing mod
decrement imported
sandbox/mod imported
Importing mod
increment imported
sandbox/mod imported
mod1 0
mod2 0
mod1 -1
mod2 1
trace(`Compartment imported\n`);
const XSCompartment = globalThis.Compartment;
trace('Compartment.map: ' + Object.keys(XSCompartment.map)+'\n');
// Mappin utility function.
// Create the child XS map from the parent XS map.
function createXSMap(modules) {
// Need to at least allow new compartments to be created.
const result = { Compartment: XSCompartment.map.Compartment };
// Only support wildcard referrer for now.
const referer = '*';
const targets = modules[referer];
for (const [specifier, target] of Object.entries(targets)) {
if (specifier === 'Compartment') {
throw new TypeError('Module not accessible to userland');
}
result[specifier] = XSCompartment.map[target];
}
return result;
}
export default class Compartment {
#instance; // engine
constructor(globals, modules) {
this.#instance = new XSCompartment(
'Compartment',
globals, // no special handling required
createXSMap(modules),
);
}
get global() {
return this.#instance.global;
}
// For debugging
// static get map() {
// return XSCompartment.map;
// }
// For debugging
// get export() {
// return this.#instance.export;
// }
async import(specifier) {
trace(`Importing ${specifier}\n`);
return this.#instance.export.API.import(specifier);
}
}
// Override the global object "Compartment".
Object.defineProperties(globalThis, {
Compartment: {
value: Compartment,
enumerable: false,
configurable: true,
writable: true,
}
});
// Internal API, not visible to code.
export const API = {
async import(specifier) {
if (specifier.toString() === 'Compartment') {
throw new TypeError('Module not accessible to userland');
}
return import(specifier);
}
};
trace(`main imported\n`);
// Load the SES Compartment, the native one is no longer available.
import Compartment from 'Compartment';
// Load create the compartment for the sandbox with some powers.
const c = new Compartment({}, {
'*' : {
main: 'sandbox/main',
mod: 'sandbox/mod',
increment: 'sandbox/increment',
decrement: 'sandbox/decrement',
}
});
c.import('main');
{
"include": "$(MODDABLE)/examples/manifest_base.json",
"modules": {
"*": [
"./Compartment",
"./main",
],
"sandbox/main": "./sandbox/main",
"sandbox/mod": "./sandbox/mod",
"sandbox/increment": "./sandbox/increment",
"sandbox/decrement": "./sandbox/decrement",
},
"preload": [
],
}
trace(`decrement imported\n`);
let x = 0;
export default function() {
return x--;
}
trace(`increment imported\n`);
let x = 0;
export default function() {
return x++;
}
trace(`sandbox/main imported\n`);
// The sandbox main doesn't have access to the native Compartment XS API.
const cmp1 = new Compartment({ name: 'mod1' }, { '*': { 'mod': 'mod', 'vary': 'decrement' } });
const cmp2 = new Compartment({ name: 'mod2' }, { '*': { 'mod': 'mod', 'vary': 'increment' } });
const mod1 = await cmp1.import('mod');
const mod2 = await cmp2.import('mod');
mod1.test();
mod2.test();
mod1.test();
mod2.test();
trace(`sandbox/mod imported\n`);
import vary from "vary";
export function test() {
trace(name + " " + vary() + "\n");
}
@dckc
Copy link

dckc commented Jan 23, 2020

MM says for hosting-completeness, compartment instance should have an eval() method that takes options.

@dckc
Copy link

dckc commented Jan 23, 2020

(how) does this work across Realms?

Q (caridy): does this work across Realms?
A (@erights): this should be consistent with Realms, but I haven't been focussing on that yet...
Q(refined): what if evaluated source [again, source code is not visible in this API] I create a new realm?
A: option 1) add a new Realm constructor in each compartment...
option 2) Realm constructor is part of shared primordials, so that...
[missed]
MM: let's table that; good question.

@erights
Copy link

erights commented Jan 24, 2020

MM says for hosting-completeness, compartment instance should have an eval() method that takes options.

Actually evaluate() to keep it distinct. @FUDCo made the point yesterday that its availability from the compartment instance should not reply on its binding on the compartment's global being unmodified. Since we don't yet have any options, it can start out as an alias for the global's initial eval.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment