From e9c6152153a524d1ca6c63427bd1f3156c2fe385 Mon Sep 17 00:00:00 2001 From: Chung Shing Hin Date: Thu, 14 Mar 2024 18:06:14 +0800 Subject: [PATCH] c: Run WASM binaries in Web worker #612 --- Code.xcodeproj/project.pbxproj | 6 + CodeApp/CodeApp.swift | 7 +- CodeApp/Constants/Resources.swift | 2 +- CodeApp/Managers/Executor.swift | 5 +- CodeApp/Managers/WASMService.swift | 59 +++++++++ CodeApp/Utilities/wasm.swift | 11 +- LanguageResources/ClangLib/wasm-worker.html | 46 +++++++ LanguageResources/ClangLib/worker.js | 139 ++++++++++++++++++++ 8 files changed, 268 insertions(+), 7 deletions(-) create mode 100644 CodeApp/Managers/WASMService.swift create mode 100644 LanguageResources/ClangLib/wasm-worker.html create mode 100644 LanguageResources/ClangLib/worker.js diff --git a/Code.xcodeproj/project.pbxproj b/Code.xcodeproj/project.pbxproj index fcad8f0a3..887ef3dea 100644 --- a/Code.xcodeproj/project.pbxproj +++ b/Code.xcodeproj/project.pbxproj @@ -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, ); }; }; @@ -1631,6 +1633,7 @@ 942E320328087FA400233441 /* SearchResultsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsSection.swift; sourceTree = ""; }; 942E32062808805F00233441 /* SearchUnsupportedSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchUnsupportedSection.swift; sourceTree = ""; }; 942E321128093CA600233441 /* NMSSH.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = NMSSH.xcframework; path = Resources/NMSSH.xcframework; sourceTree = ""; }; + 9434C3EE2BA2CCBB00EB1CF6 /* WASMService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WASMService.swift; sourceTree = ""; }; 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 = ""; }; 94369B0025E3B933008419A0 /* ActionRequestHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionRequestHandler.swift; sourceTree = ""; }; @@ -2886,6 +2889,7 @@ 9FA122582A8B17E400E7B417 /* StatusBarManager.swift */, 9FA122782A8B6C9700E7B417 /* ActivityBarManager.swift */, 9F062FE52B58D40D006210AA /* FileTreeViewController.swift */, + 9434C3EE2BA2CCBB00EB1CF6 /* WASMService.swift */, ); path = Managers; sourceTree = ""; @@ -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 */, @@ -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 */, diff --git a/CodeApp/CodeApp.swift b/CodeApp/CodeApp.swift index a6f8eaadc..5479c298c 100644 --- a/CodeApp/CodeApp.swift +++ b/CodeApp/CodeApp.swift @@ -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") { @@ -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() diff --git a/CodeApp/Constants/Resources.swift b/CodeApp/Constants/Resources.swift index d7834945c..fa58bd203 100644 --- a/CodeApp/Constants/Resources.swift +++ b/CodeApp/Constants/Resources.swift @@ -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") diff --git a/CodeApp/Managers/Executor.swift b/CodeApp/Managers/Executor.swift index d196db164..7753f34ba 100644 --- a/CodeApp/Managers/Executor.swift +++ b/CodeApp/Managers/Executor.swift @@ -85,7 +85,10 @@ class Executor { CFNotificationCenterPostNotification( notificationCenter, notificationName, nil, nil, false) } - + if javascriptRunning { + javascriptRunning = false + return + } ios_switchSession(persistentIdentifier.toCString()) ios_kill() } diff --git a/CodeApp/Managers/WASMService.swift b/CodeApp/Managers/WASMService.swift new file mode 100644 index 000000000..7e851405b --- /dev/null +++ b/CodeApp/Managers/WASMService.swift @@ -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, + ]) + } +} diff --git a/CodeApp/Utilities/wasm.swift b/CodeApp/Utilities/wasm.swift index fc5f3c8f8..d732b8453 100644 --- a/CodeApp/Utilities/wasm.swift +++ b/CodeApp/Utilities/wasm.swift @@ -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) { @@ -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( @@ -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 { diff --git a/LanguageResources/ClangLib/wasm-worker.html b/LanguageResources/ClangLib/wasm-worker.html new file mode 100644 index 000000000..a13d0dbba --- /dev/null +++ b/LanguageResources/ClangLib/wasm-worker.html @@ -0,0 +1,46 @@ + + + + Code - WebAssembly Execution + + + + + \ No newline at end of file diff --git a/LanguageResources/ClangLib/worker.js b/LanguageResources/ClangLib/worker.js new file mode 100644 index 000000000..6a1ef59d8 --- /dev/null +++ b/LanguageResources/ClangLib/worker.js @@ -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; \ No newline at end of file