Extension Structure
A compiled extension follows a specific structure that integrates with RemixWarp's compiler architecture. Understanding this structure is essential for creating effective compiled extensions.
Basic Extension Template
Every compiled extension starts with this fundamental structure:
Extension Declaration
The extension begins by checking for the unsandboxed environment, which is required for accessing compiler internals:
(function(Scratch) {
'use strict';
if (!Scratch.extensions.unsandboxed) {
throw new Error("Extension needs to be run unsandboxed.");
}
Accessing Compiler Internals
Next, the extension obtains references to RemixWarp's compiler components:
const { vm, BlockType, ArgumentType } = Scratch;
const { runtime } = vm;
// Access the compiler internals
const iwnafhwtb = vm.exports.i_will_not_ask_for_help_when_these_break();
const { JSGenerator, IRGenerator, ScriptTreeGenerator } = iwnafhwtb;
The i_will_not_ask_for_help_when_these_break() function provides access to internal APIs that may change between versions. The name serves as a warning that these APIs are not stable.
Compiler Type System
Compiled extensions work with RemixWarp's type system for optimization:
const {
TYPE_NUMBER,
TYPE_STRING,
TYPE_BOOLEAN,
TYPE_UNKNOWN,
TYPE_NUMBER_NAN,
TypedInput,
ConstantInput,
VariableInput,
Frame,
sanitize
} = JSGenerator.unstable_exports;
Patching System
Compiled extensions use a patching system to modify compiler behavior. The patching function ensures multiple extensions can coexist:
Patch Function
const PATCHES_ID = 'extensionname_patches';
const cst_patch = (obj, functions) => {
if (obj[PATCHES_ID]) return; // Prevent double-patching
obj[PATCHES_ID] = {};
for (const name in functions) {
const original = obj[name];
obj[PATCHES_ID][name] = obj[name];
if (original) {
obj[name] = function(...args) {
const callOriginal = (...args) => original.call(this, ...args);
return functions[name].call(this, callOriginal, ...args);
};
} else {
obj[name] = function(...args) {
return functions[name].call(this, () => {}, ...args);
};
}
}
};
This system allows extensions to override compiler methods while preserving the ability to call the original implementation.
Compiler Phase Patches
JavaScript Generation Patch
The JSGenerator patch handles converting blocks into final JavaScript code:
cst_patch(JSGenerator.prototype, {
descendStackedBlock(fn, node, ...args) {
let b = node.block;
switch (node.kind) {
case 'myextension.myblock':
const input1 = this.descendInput(node.INPUT1).asNumber();
const input2 = this.descendInput(node.INPUT2).asString();
this.source += `vm.runtime.visualReport("${b.id}", Math.pow(${input1}, 2))\n`;
return;
default:
return fn(node, ...args);
}
},
descendInput(fn, node, ...args) {
switch (node.kind) {
case 'myextension.myblock':
const input1 = this.descendInput(node.INPUT1).asNumber();
return new TypedInput(`Math.pow(${input1}, 2)`, TYPE_NUMBER);
default:
return fn(node, ...args);
}
}
});
Script Tree Generation Patch
The ScriptTreeGenerator patch identifies extension blocks and prepares them for compilation:
cst_patch(ScriptTreeGenerator.prototype, {
descendStackedBlock(fn, block, ...args) {
switch (block.opcode) {
case 'myextension_myblock':
return {
block,
kind: 'myextension.myblock',
INPUT1: this.descendInputOfBlock(block, 'INPUT1'),
INPUT2: this.descendInputOfBlock(block, 'INPUT2'),
};
default:
return fn(block, ...args);
}
},
descendInput(fn, block, ...args) {
// Similar structure for reporter blocks
switch (block.opcode) {
case 'myextension_myblock':
return {
block,
kind: 'myextension.myblock',
INPUT1: this.descendInputOfBlock(block, 'INPUT1'),
INPUT2: this.descendInputOfBlock(block, 'INPUT2'),
};
default:
return fn(block, ...args);
}
}
});
Extension Class Definition
After setting up the patches, define the extension class:
Basic Class Structure
class MyExtension {
getInfo() {
return {
id: 'myextension',
name: 'My Extension',
color1: '#2DA4A0',
version: 1.0,
blocks: [
{
opcode: 'myblock',
text: 'calculate [INPUT1] squared',
blockType: Scratch.BlockType.REPORTER,
arguments: {
INPUT1: {
type: Scratch.ArgumentType.NUMBER,
defaultValue: 5
}
},
func: 'fallbackFunction'
}
]
};
}
fallbackFunction(args) {
// Fallback implementation for non-compiled environments
return Math.pow(args.INPUT1, 2);
}
}