Skip to content

Commit

Permalink
c: Run WASM binaries in Web worker thebaselab#612
Browse files Browse the repository at this point in the history
  • Loading branch information
bummoblizard committed Mar 14, 2024
1 parent 358e1b5 commit e9c6152
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 7 deletions.
6 changes: 6 additions & 0 deletions Code.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,8 @@
942E320528087FA400233441 /* SearchResultsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 942E320328087FA400233441 /* SearchResultsSection.swift */; };
942E32072808805F00233441 /* SearchUnsupportedSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 942E32062808805F00233441 /* SearchUnsupportedSection.swift */; };
942E32082808805F00233441 /* SearchUnsupportedSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 942E32062808805F00233441 /* SearchUnsupportedSection.swift */; };
9434C3EF2BA2CCBB00EB1CF6 /* WASMService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9434C3EE2BA2CCBB00EB1CF6 /* WASMService.swift */; };
9434C3F02BA2CCBB00EB1CF6 /* WASMService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9434C3EE2BA2CCBB00EB1CF6 /* WASMService.swift */; };
94369AFF25E3B933008419A0 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 94369AFE25E3B933008419A0 /* Media.xcassets */; };
94369B0125E3B933008419A0 /* ActionRequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94369B0025E3B933008419A0 /* ActionRequestHandler.swift */; };
94369B0725E3B933008419A0 /* extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 94369AFC25E3B933008419A0 /* extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
Expand Down Expand Up @@ -1631,6 +1633,7 @@
942E320328087FA400233441 /* SearchResultsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsSection.swift; sourceTree = "<group>"; };
942E32062808805F00233441 /* SearchUnsupportedSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchUnsupportedSection.swift; sourceTree = "<group>"; };
942E321128093CA600233441 /* NMSSH.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = NMSSH.xcframework; path = Resources/NMSSH.xcframework; sourceTree = "<group>"; };
9434C3EE2BA2CCBB00EB1CF6 /* WASMService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WASMService.swift; sourceTree = "<group>"; };
94369AFC25E3B933008419A0 /* extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = extension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
94369AFE25E3B933008419A0 /* Media.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Media.xcassets; sourceTree = "<group>"; };
94369B0025E3B933008419A0 /* ActionRequestHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionRequestHandler.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2886,6 +2889,7 @@
9FA122582A8B17E400E7B417 /* StatusBarManager.swift */,
9FA122782A8B6C9700E7B417 /* ActivityBarManager.swift */,
9F062FE52B58D40D006210AA /* FileTreeViewController.swift */,
9434C3EE2BA2CCBB00EB1CF6 /* WASMService.swift */,
);
path = Managers;
sourceTree = "<group>";
Expand Down Expand Up @@ -3293,6 +3297,7 @@
942E32082808805F00233441 /* SearchUnsupportedSection.swift in Sources */,
94A347BE293DE24B00A59658 /* hiddenSystemOverlays.swift in Sources */,
9F3C2DC92918A31000BFF14C /* hiddenScrollableContentBackground.swift in Sources */,
9434C3F02BA2CCBB00EB1CF6 /* WASMService.swift in Sources */,
945D4F592B50F55800DE0DBA /* VimKeyBufferLabel.swift in Sources */,
9FC03E6A291F303900DECD1B /* Utilities.swift in Sources */,
9419695D280316C7008AAEB2 /* TopBar.swift in Sources */,
Expand Down Expand Up @@ -3470,6 +3475,7 @@
942E32072808805F00233441 /* SearchUnsupportedSection.swift in Sources */,
94A347BD293DE24B00A59658 /* hiddenSystemOverlays.swift in Sources */,
9F3C2DC82918A31000BFF14C /* hiddenScrollableContentBackground.swift in Sources */,
9434C3EF2BA2CCBB00EB1CF6 /* WASMService.swift in Sources */,
945D4F582B50F55800DE0DBA /* VimKeyBufferLabel.swift in Sources */,
9FC03E69291F303900DECD1B /* Utilities.swift in Sources */,
94D721DF268DA2B8007A63BD /* TopBar.swift in Sources */,
Expand Down
7 changes: 4 additions & 3 deletions CodeApp/CodeApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import ios_system
@main
struct CodeApp: App {
@StateObject var themeManager = ThemeManager()
let wasmService = WASMService()

func versionNumberIncreased() -> Bool {
if let lastReadVersion = UserDefaults.standard.string(forKey: "changelog.lastread") {
Expand Down Expand Up @@ -296,9 +297,9 @@ struct CodeApp: App {
}

DispatchQueue.main.async {
wasmWebView.loadFileURL(
Resources.wasmHTML,
allowingReadAccessTo: Resources.wasmHTML)
let request = URLRequest(
url: URL(string: "http://localhost:\(String(WASMService.PORT))/wasm-worker.html")!)
wasmWebView.load(request)
}
initializeEnvironment()
Repository.initialize_libgit2()
Expand Down
2 changes: 1 addition & 1 deletion CodeApp/Constants/Resources.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class Resources {

static let wasmHTML = URL(
fileURLWithPath: Bundle.main.path(
forResource: "wasm", ofType: "html", inDirectory: "ClangLib")!)
forResource: "wasm-worker", ofType: "html", inDirectory: "ClangLib")!)

static let themes = URL(fileURLWithPath: Bundle.main.resourcePath!).appendingPathComponent(
"Themes")
Expand Down
5 changes: 4 additions & 1 deletion CodeApp/Managers/Executor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,10 @@ class Executor {
CFNotificationCenterPostNotification(
notificationCenter, notificationName, nil, nil, false)
}

if javascriptRunning {
javascriptRunning = false
return
}
ios_switchSession(persistentIdentifier.toCString())
ios_kill()
}
Expand Down
59 changes: 59 additions & 0 deletions CodeApp/Managers/WASMService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//
// WASMService.swift
// Code
//
// Created by Ken Chung on 14/03/2024.
//

import GCDWebServers

class WASMService {
static let PORT = 20233
private let webServer = GCDWebServer()

init() {
let basePath = "/"
let directoryPath = Resources.wasmHTML.deletingLastPathComponent().path + "/"
webServer.addHandler(
match: { requestMethod, requestURL, requestHeaders, urlPath, urlQuery in
if requestMethod != "GET" {
return nil
}
if !urlPath.hasPrefix(basePath) {
return nil
}
return GCDWebServerRequest(
method: requestMethod, url: requestURL, headers: requestHeaders, path: urlPath,
query: urlQuery)
},
processBlock: { request in
let filePath =
directoryPath
+ GCDWebServerNormalizePath(String(request.path.dropFirst(basePath.count)))
var isDirectory: ObjCBool = false
guard FileManager.default.fileExists(atPath: filePath, isDirectory: &isDirectory),
!isDirectory.boolValue
else {
return GCDWebServerResponse(
statusCode: GCDWebServerClientErrorHTTPStatusCode.httpStatusCode_NotFound
.rawValue)
}
let response = GCDWebServerFileResponse(
file: filePath, byteRange: request.byteRange)
response?.setValue("bytes", forAdditionalHeader: "Accept-Ranges")
response?.setValue("same-origin", forAdditionalHeader: "Cross-Origin-Opener-Policy")
response?.setValue(
"require-corp", forAdditionalHeader: "Cross-Origin-Embedder-Policy")
response?.setValue(
"same-origin", forAdditionalHeader: "Cross-Origin-Resource-Policy")
response?.cacheControlMaxAge = 10
return response
})

try? webServer.start(options: [
GCDWebServerOption_AutomaticallySuspendInBackground: true,
GCDWebServerOption_BindToLocalhost: true,
GCDWebServerOption_Port: WASMService.PORT,
])
}
}
11 changes: 9 additions & 2 deletions CodeApp/Utilities/wasm.swift
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,9 @@ class WasmWebView: WKWebView {
self.navigationDelegate = delegate
self.uiDelegate = delegate
self.isAccessibilityElement = false
if #available(iOS 16.4, *) {
self.isInspectable = true
}
}

required init?(coder: NSCoder) {
Expand Down Expand Up @@ -582,7 +585,8 @@ private func executeWebAssembly(arguments: [String]?) -> Int32 {
environmentAsJSDictionary += "}"
let base64string = buffer.base64EncodedString()
let javascript =
"executeWebAssembly(\"\(base64string)\", " + argumentString + ", \"" + currentDirectory
"return await executeWebAssembly(\"\(base64string)\", " + argumentString + ", \""
+ currentDirectory
+ "\", \(ios_isatty(STDIN_FILENO)), " + environmentAsJSDictionary + ")"
if javascriptRunning {
fputs(
Expand All @@ -596,7 +600,10 @@ private func executeWebAssembly(arguments: [String]?) -> Int32 {
thread_stdout_copy = thread_stdout
thread_stderr_copy = thread_stderr
DispatchQueue.main.async {
wasmWebView.evaluateJavaScript(javascript) { (result, error) in
wasmWebView.callAsyncJavaScript(javascript, arguments: [:], in: nil, in: .page) {
result in
let result = try? result.get()

if result != nil {
// executeWebAssembly sends back stdout and stderr as two Strings:
if let array = result! as? NSMutableArray {
Expand Down
46 changes: 46 additions & 0 deletions LanguageResources/ClangLib/wasm-worker.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<title>Code - WebAssembly Execution</title>
</head>
<body>
<script>
var messageHandler = (e) => {
const action = e.data[0];

switch (action) {
case "prompt":
const int32 = new Int32Array(e.data[2]);;
const str = prompt(e.data[1]);
for (var i=0, strLen=str.length; i<strLen; i++) {
Atomics.store(int32, i, str.charCodeAt(i));
}
Atomics.notify(int32, 0, 1);
break;
case "print":
window.webkit.messageHandlers.aShell.postMessage('print:' + e.data[1]);
break;
case "println":
window.webkit.messageHandlers.aShell.postMessage('print:' + e.data[1] + "\n");
break;
case "print_error":
window.webkit.messageHandlers.aShell.postMessage('print_error:' + e.data[1]);
break;
default:
console.error(`${action} not hanlded`);
}
};

async function executeWebAssembly(bufferString, args, cwd, tty, env) {
const worker = new Worker("worker.js");
worker.onmessage = messageHandler;
const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 8192);
const int32 = new Int32Array(sab);
worker.postMessage([bufferString, args, cwd, tty, env, sab]);
await Atomics.waitAsync(int32, 0, 0, 120000).value;
worker.terminate();
return int32[0];
}
</script>
</body>
</html>
139 changes: 139 additions & 0 deletions LanguageResources/ClangLib/worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
importScripts("require.js");

// make the "require" function available to all
Tarp.require({expose: true});
// Have a global variable:
var window = {};
var global = {};
window.global = window;
// and a Buffer variable
var Buffer = require('buffer').Buffer;
var process = require('process');

// Functions to deal with WebAssembly:
// These should load a wasm program: http://andrewsweeney.net/post/llvm-to-wasm/
/* Array of bytes to base64 string decoding */
// Modules for @wasmer:
const WASI = require('@wasmer/wasi/lib').WASI;
const browserBindings = require('@wasmer/wasi/lib/bindings/browser').default;
const WasmFs = require('@wasmer/wasmfs').WasmFs;
const lowerI64Imports = require("@wasmer/wasm-transformer").lowerI64Imports

function b64ToUint6 (nChr) {

return nChr > 64 && nChr < 91 ?
nChr - 65
: nChr > 96 && nChr < 123 ?
nChr - 71
: nChr > 47 && nChr < 58 ?
nChr + 4
: nChr === 43 ?
62
: nChr === 47 ?
63
:
0;

}

function base64DecToArr (sBase64, nBlockSize) {
var
sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), nInLen = sB64Enc.length,
nOutLen = nBlockSize ? Math.ceil((nInLen * 3 + 1 >>> 2) / nBlockSize) * nBlockSize : nInLen * 3 + 1 >>> 2, aBytes = new Uint8Array(nOutLen);

for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) {
nMod4 = nInIdx & 3;
nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4;
if (nMod4 === 3 || nInLen - nInIdx === 1) {
for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
aBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255;
}
nUint24 = 0;
}
}
return aBytes;
}

// bufferString: program in base64 format
// args: arguments (argv[argc])
// stdinBuffer: standard input
// cwd: current working directory
function executeWebAssembly(bufferString, args, cwd, tty, env) {
// Input: base64 encoded binary wasm file
// if (!('WebAssembly' in window)) {
// window.webkit.messageHandlers.aShell.postMessage('WebAssembly not supported');
// return;
// }

var arrayBuffer = base64DecToArr(bufferString);
const loweredWasmBytes = lowerI64Imports(arrayBuffer);
var errorMessage = '';
var errorCode = 0;
// window.standardInput = '';
// TODO: link with other libraries/frameworks? impossible, I guess.
// TODO: keyboard input (directly from onkeypress)
try {
const wasmFs = new WasmFs(); // local file system. Used less often.
let wasi = new WASI({
preopens: {'.': cwd, '/': '/'},
args: args,
env: env,
bindings: {
...browserBindings,
fs: wasmFs.fs,
}
})
wasi.args = args
if (tty != 1) {
wasi.bindings.isTTY = (fd) => false;
}
const module = new WebAssembly.Module(loweredWasmBytes);
const instance = new WebAssembly.Instance(module, wasi.getImports(module));
wasi.start(instance);
}
catch (error) {
// window.webkit.messageHandlers.aShell.postMessage('WebAssembly error: ' + error);
// WASI returns an error even in some cases where things went well.
// We find the type of the error, and return the appropriate error message
if (error.code === 'undefined') {
errorCode = 1;
errorMessage = '\nwasm: ' + error + '\n';
} else if (error.code != null) {
// Numerical error code. Send the return code back to Swift.
errorCode = error.code;
} else {
errorCode = 1;
}
}
return [errorCode, errorMessage];
}

println = (m) => {
postMessage(["println", m]);
};

print_error = (m) => {
postMessage(["print_error", m]);
};

prompt = (p) => {
const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 8192);
const int32 = new Int32Array(sab);
postMessage(["prompt", p, sab]);
Atomics.wait(int32, 0, 0);
const firstZeroIndex = int32.indexOf(0);
return String.fromCharCode.apply(null, int32.subarray(0, firstZeroIndex));
}

onmessage = (e) => {
const int32 = new Int32Array(e.data[5]);

executeWebAssembly(e.data[0], e.data[1], e.data[2], e.data[3], e.data[4]);

// Finishing execution
Atomics.store(int32, 0, 1);
Atomics.notify(int32, 0, 1);
};

console.log = println;
console.error = print_error;

0 comments on commit e9c6152

Please sign in to comment.