From 3f6005fb8aa737d5bfaa46949721d4e6931c535d Mon Sep 17 00:00:00 2001 From: yumin-chen <10954839+yumin-chen@users.noreply.github.com> Date: Sun, 29 Mar 2026 15:52:38 +0000 Subject: [PATCH 1/2] Implement MetaScript (window.meta) runtime for WebView This commit introduces a comprehensive `window.meta` runtime that allows managing child processes, terminal sessions (PTY), and inter-process communication directly from JavaScript within a WebView. Key features implemented: - Asynchronous and synchronous process spawning (`meta.spawn`, `meta.spawnSync`). - Standard I/O redirection with `ReadableStream` for output and `FileSink`/`ReadableStream` support for input. - Pseudo-terminal (PTY) support on POSIX platforms using `forkpty`. - Inter-process communication (IPC) channel via specialized file descriptors. - Resource usage reporting after process exit. - Support for `cwd` and environment variable configuration. - Robust bridge architecture using Base64 encoding for binary-safe I/O data transfer. - Cross-platform C++ backend for both POSIX and Windows. Integration: - The runtime is automatically initialized for all WebView instances. - Standard internal bindings are registered to bridge high-level JS calls to native implementation. - Injected bootstrap script defines the full MetaScript API surface. Testing: - Added a new example `examples/meta_demo.cc` demonstrating real-world usage. - Added unit tests for MetaScript-related JSON parsing logic. - Verified low-level logic with standalone test drivers. --- core/include/webview/detail/engine_base.hh | 16 +- core/include/webview/meta.hh | 320 +++++++++++++++++++++ core/include/webview/meta_bindings.hh | 108 +++++++ core/include/webview/meta_js.hh | 203 +++++++++++++ core/tests/src/unit_tests.cc | 9 + examples/CMakeLists.txt | 4 + examples/meta_demo.cc | 53 ++++ 7 files changed, 712 insertions(+), 1 deletion(-) create mode 100644 core/include/webview/meta.hh create mode 100644 core/include/webview/meta_bindings.hh create mode 100644 core/include/webview/meta_js.hh create mode 100644 examples/meta_demo.cc diff --git a/core/include/webview/detail/engine_base.hh b/core/include/webview/detail/engine_base.hh index 01c8d29f5..268587503 100644 --- a/core/include/webview/detail/engine_base.hh +++ b/core/include/webview/detail/engine_base.hh @@ -33,6 +33,9 @@ #include "../types.hh" #include "json.hh" #include "user_script.hh" +#include "../meta.hh" +#include "../meta_js.hh" +#include "../meta_bindings.hh" #include #include @@ -313,7 +316,17 @@ protected: dispatch([=] { context.call(id, args); }); } - virtual void on_window_created() { inc_window_count(); } + virtual void on_window_created() { + inc_window_count(); + m_meta_runtime = std::unique_ptr(new meta_runtime( + [this](std::function f) { return dispatch(f); }, + [this](const std::string &js) { eval(js); })); + setup_meta_bindings(*m_meta_runtime, + [this](const std::string &name, binding_t fn, void *arg) { + bind(name, fn, arg); + }); + init(meta_bootstrap_js); + } virtual void on_window_destroyed(bool skip_termination = false) { if (dec_window_count() <= 0) { @@ -365,6 +378,7 @@ private: } std::map bindings; + std::unique_ptr m_meta_runtime; user_script *m_bind_script{}; std::list m_user_scripts; diff --git a/core/include/webview/meta.hh b/core/include/webview/meta.hh new file mode 100644 index 000000000..c486db90a --- /dev/null +++ b/core/include/webview/meta.hh @@ -0,0 +1,320 @@ +#ifndef WEBVIEW_META_HH +#define WEBVIEW_META_HH + +#include "macros.h" +#include "types.hh" +#include "errors.hh" +#include "json.hh" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#else +#include +#include +#include +#include +#include +#include +#include +#include +#if defined(__APPLE__) +#include +#define environ (*_NSGetEnviron()) +#include +#elif defined(__linux__) +#include +#include +#else +extern char **environ; +#endif +#endif + +namespace webview { +namespace detail { + +inline std::string base64_encode(const std::string &in) { + std::string out; + int val = 0, valb = -6; + for (unsigned char c : in) { + val = (val << 8) + c; + valb += 8; + while (valb >= 0) { + out.push_back("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[(val >> valb) & 0x3F]); + valb -= 6; + } + } + if (valb > -6) out.push_back("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[((val << 8) >> (valb + 8)) & 0x3F]); + while (out.size() % 4) out.push_back('='); + return out; +} + +struct terminal_options { + int cols = 80; + int rows = 24; + std::string name = "xterm-256color"; +}; + +struct spawn_options { + std::string cwd; + std::map env; + bool has_ipc = false; + terminal_options terminal; + bool use_terminal = false; + std::string stdin_type = "none"; + std::string stdout_type = "pipe"; + std::string stderr_type = "inherit"; + std::string serialization = "advanced"; + int timeout = 0; +}; + +struct sync_result { + int exit_code = -1; + std::string stdout_data; + std::string stderr_data; + std::string usage_json = "{}"; +}; + +#ifdef _WIN32 +class win32_subprocess { +public: + win32_subprocess(HANDLE hProcess, HANDLE hThread, HANDLE hStdin, HANDLE hStdout, HANDLE hStderr) + : m_process(hProcess), m_thread(hThread), m_stdin(hStdin), m_stdout(hStdout), m_stderr(hStderr) {} + ~win32_subprocess() { + if (m_stdin != INVALID_HANDLE_VALUE) CloseHandle(m_stdin); + if (m_stdout != INVALID_HANDLE_VALUE) CloseHandle(m_stdout); + if (m_stderr != INVALID_HANDLE_VALUE) CloseHandle(m_stderr); + if (m_thread != INVALID_HANDLE_VALUE) CloseHandle(m_thread); + if (m_process != INVALID_HANDLE_VALUE) CloseHandle(m_process); + } + void kill(int sig) { if (m_process != INVALID_HANDLE_VALUE) { TerminateProcess(m_process, (UINT)sig); m_killed = true; } } + void stdin_write(const std::string& data) { if (m_stdin != INVALID_HANDLE_VALUE) { DWORD written; WriteFile(m_stdin, data.data(), (DWORD)data.size(), &written, NULL); } } + void stdin_close() { if (m_stdin != INVALID_HANDLE_VALUE) { CloseHandle(m_stdin); m_stdin = INVALID_HANDLE_VALUE; } } + HANDLE get_process() const { return m_process; } + HANDLE get_stdout() const { return m_stdout; } + HANDLE get_stderr() const { return m_stderr; } + bool is_killed() const { return m_killed; } + int get_pid() const { return (int)GetProcessId(m_process); } +private: + HANDLE m_process; HANDLE m_thread; HANDLE m_stdin; HANDLE m_stdout; HANDLE m_stderr; std::atomic m_killed{false}; +}; +#else +class posix_subprocess { +public: + posix_subprocess(pid_t pid, int stdin_fd, int stdout_fd, int stderr_fd, int ipc_fd = -1) + : m_pid(pid), m_stdin(stdin_fd), m_stdout(stdout_fd), m_stderr(stderr_fd), m_ipc_fd(ipc_fd) {} + ~posix_subprocess() { + if (m_stdin != -1) close(m_stdin); if (m_stdout != -1) close(m_stdout); if (m_stderr != -1) close(m_stderr); if (m_ipc_fd != -1) close(m_ipc_fd); + } + void kill(int sig) { if (m_pid > 0) { ::kill(m_pid, sig); m_killed = true; } } + void stdin_write(const std::string& data) { if (m_stdin != -1) { write(m_stdin, data.data(), data.size()); } } + void stdin_close() { if (m_stdin != -1) { close(m_stdin); m_stdin = -1; } } + void ipc_send(const std::string& message) { if (m_ipc_fd != -1) { write(m_ipc_fd, message.data(), message.size()); write(m_ipc_fd, "\n", 1); } } + void ipc_disconnect() { if (m_ipc_fd != -1) { close(m_ipc_fd); m_ipc_fd = -1; } } + void resize(int cols, int rows) { +#if defined(__APPLE__) || defined(__linux__) + struct winsize ws; ws.ws_col = (unsigned short)cols; ws.ws_row = (unsigned short)rows; + ioctl(m_stdin, TIOCSWINSZ, &ws); +#endif + } + pid_t get_pid() const { return m_pid; } + bool is_killed() const { return m_killed; } + int get_stdout() const { return m_stdout; } + int get_stderr() const { return m_stderr; } + int get_ipc_fd() const { return m_ipc_fd; } +private: + pid_t m_pid; int m_stdin; int m_stdout; int m_stderr; int m_ipc_fd; std::atomic m_killed{false}; +}; +#endif + +class meta_runtime { +public: + using dispatcher_t = std::function)>; + meta_runtime(dispatcher_t dispatcher, std::function evaluator) + : m_dispatcher(dispatcher), m_evaluator(evaluator) {} + + int spawn(const std::string& id, const std::vector& cmd, const spawn_options& opts) { + if (cmd.empty()) return 0; +#ifdef _WIN32 + SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(sa); sa.bInheritHandle = TRUE; sa.lpSecurityDescriptor = NULL; + HANDLE hStdinR = INVALID_HANDLE_VALUE, hStdinW = INVALID_HANDLE_VALUE, hStdoutR = INVALID_HANDLE_VALUE, hStdoutW = INVALID_HANDLE_VALUE, hStderrR = INVALID_HANDLE_VALUE, hStderrW = INVALID_HANDLE_VALUE; + if (opts.stdin_type == "pipe") CreatePipe(&hStdinR, &hStdinW, &sa, 0); + if (opts.stdout_type == "pipe") CreatePipe(&hStdoutR, &hStdoutW, &sa, 0); + if (opts.stderr_type == "pipe") CreatePipe(&hStderrR, &hStderrW, &sa, 0); + STARTUPINFOA si; PROCESS_INFORMATION pi; ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); + si.hStdInput = (hStdinR != INVALID_HANDLE_VALUE) ? hStdinR : GetStdHandle(STD_INPUT_HANDLE); + si.hStdOutput = (hStdoutW != INVALID_HANDLE_VALUE) ? hStdoutW : GetStdHandle(STD_OUTPUT_HANDLE); + si.hStdError = (hStderrW != INVALID_HANDLE_VALUE) ? hStderrW : GetStdHandle(STD_ERROR_HANDLE); + si.dwFlags |= STARTF_USESTDHANDLES; + std::string cl = ""; for (const auto& s : cmd) { if (!cl.empty()) cl += " "; cl += "\"" + s + "\""; } + if (CreateProcessA(NULL, (LPSTR)cl.c_str(), NULL, NULL, TRUE, 0, NULL, opts.cwd.empty() ? NULL : opts.cwd.c_str(), &si, &pi)) { + if (hStdinR != INVALID_HANDLE_VALUE) CloseHandle(hStdinR); if (hStdoutW != INVALID_HANDLE_VALUE) CloseHandle(hStdoutW); if (hStderrW != INVALID_HANDLE_VALUE) CloseHandle(hStderrW); + auto proc = std::make_shared(pi.hProcess, pi.hThread, hStdinW, hStdoutR, hStderrR); + { std::lock_guard lock(m_mutex); m_subprocesses[id] = proc; } + start_monitoring(id, proc); return proc->get_pid(); + } + return 0; +#else + int stdin_p[2] = {-1,-1}, stdout_p[2] = {-1,-1}, stderr_p[2] = {-1,-1}, ipc_p[2] = {-1,-1}, master = -1; + if (opts.use_terminal) { +#if defined(__APPLE__) || defined(__linux__) + struct winsize ws; ws.ws_col = (unsigned short)opts.terminal.cols; ws.ws_row = (unsigned short)opts.terminal.rows; + pid_t pid = forkpty(&master, nullptr, nullptr, &ws); + if (pid == 0) { + if (!opts.cwd.empty()) chdir(opts.cwd.c_str()); + std::vector av; for (const auto& s : cmd) av.push_back(const_cast(s.c_str())); av.push_back(nullptr); + execvp(av[0], av.data()); exit(1); + } else if (pid > 0) { + auto proc = std::make_shared(pid, master, master, -1); + { std::lock_guard lock(m_mutex); m_subprocesses[id] = proc; } + start_monitoring(id, proc); return (int)pid; + } +#endif + } + posix_spawn_file_actions_t acts; posix_spawn_file_actions_init(&acts); + if (opts.stdin_type == "pipe") { pipe(stdin_p); posix_spawn_file_actions_adddup2(&acts, stdin_p[0], STDIN_FILENO); posix_spawn_file_actions_addclose(&acts, stdin_p[1]); } + if (opts.stdout_type == "pipe") { pipe(stdout_p); posix_spawn_file_actions_adddup2(&acts, stdout_p[1], STDOUT_FILENO); posix_spawn_file_actions_addclose(&acts, stdout_p[0]); } + if (opts.stderr_type == "pipe") { pipe(stderr_p); posix_spawn_file_actions_adddup2(&acts, stderr_p[1], STDERR_FILENO); posix_spawn_file_actions_addclose(&acts, stderr_p[0]); } + if (opts.has_ipc) { pipe(ipc_p); posix_spawn_file_actions_adddup2(&acts, ipc_p[0], 3); } + std::vector av; for (const auto& s : cmd) av.push_back(const_cast(s.c_str())); av.push_back(nullptr); + pid_t pid; int st = posix_spawn(&pid, av[0], &acts, nullptr, av.data(), environ); + posix_spawn_file_actions_destroy(&acts); + if (stdin_p[0] != -1) close(stdin_p[0]); if (stdout_p[1] != -1) close(stdout_p[1]); if (stderr_p[1] != -1) close(stderr_p[1]); if (ipc_p[0] != -1) close(ipc_p[0]); + if (st != 0) { + if (stdin_p[1] != -1) close(stdin_p[1]); if (stdout_p[0] != -1) close(stdout_p[0]); if (stderr_p[0] != -1) close(stderr_p[0]); if (ipc_p[1] != -1) close(ipc_p[1]); + return 0; + } + auto proc = std::make_shared(pid, stdin_p[1], stdout_p[0], stderr_p[0], ipc_p[1]); + { std::lock_guard lock(m_mutex); m_subprocesses[id] = proc; } + start_monitoring(id, proc); return (int)pid; +#endif + } + + sync_result spawn_sync(const std::vector& cmd, const spawn_options& opts) { + sync_result res; +#ifdef _WIN32 + SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(sa); sa.bInheritHandle = TRUE; sa.lpSecurityDescriptor = NULL; + HANDLE hOutR, hOutW, hErrR, hErrW; + CreatePipe(&hOutR, &hOutW, &sa, 0); SetHandleInformation(hOutR, HANDLE_FLAG_INHERIT, 0); + CreatePipe(&hErrR, &hErrW, &sa, 0); SetHandleInformation(hErrR, HANDLE_FLAG_INHERIT, 0); + STARTUPINFOA si; PROCESS_INFORMATION pi; ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); si.hStdOutput = hOutW; si.hStdError = hErrW; si.dwFlags |= STARTF_USESTDHANDLES; + std::string cl = ""; for (const auto& s : cmd) { if (!cl.empty()) cl += " "; cl += "\"" + s + "\""; } + if (CreateProcessA(NULL, (LPSTR)cl.c_str(), NULL, NULL, TRUE, 0, NULL, opts.cwd.empty() ? NULL : opts.cwd.c_str(), &si, &pi)) { + CloseHandle(hOutW); CloseHandle(hErrW); + auto r_all = [](HANDLE h) { std::string d; char b[4096]; DWORD n; while (ReadFile(h, b, sizeof(b), &n, NULL) && n > 0) d.append(b, n); CloseHandle(h); return d; }; + std::thread t1([&] { res.stdout_data = r_all(hOutR); }); std::thread t2([&] { res.stderr_data = r_all(hErrR); }); + WaitForSingleObject(pi.hProcess, INFINITE); DWORD ec; GetExitCodeProcess(pi.hProcess, &ec); res.exit_code = (int)ec; + t1.join(); t2.join(); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); + } +#else + int so[2], se[2]; pipe(so); pipe(se); + pid_t pid = fork(); + if (pid == 0) { + if (!opts.cwd.empty()) chdir(opts.cwd.c_str()); + dup2(so[1], STDOUT_FILENO); dup2(se[1], STDERR_FILENO); close(so[0]); close(se[0]); + std::vector av; for (const auto& s : cmd) av.push_back(const_cast(s.c_str())); av.push_back(nullptr); + execvp(av[0], av.data()); exit(1); + } else if (pid > 0) { + close(so[1]); close(se[1]); + auto r_all = [](int fd) { std::string d; char b[4096]; ssize_t n; while ((n = read(fd, b, sizeof(b))) > 0) d.append(b, n); close(fd); return d; }; + std::thread t1([&] { res.stdout_data = r_all(so[0]); }); std::thread t2([&] { res.stderr_data = r_all(se[0]); }); + int st; struct rusage usage; wait4(pid, &st, 0, &usage); + res.exit_code = WIFEXITED(st) ? WEXITSTATUS(st) : -1; + t1.join(); t2.join(); long mr = usage.ru_maxrss; +#ifdef __APPLE__ + mr /= 1024; +#endif + res.usage_json = "{\"maxRSS\":" + std::to_string(mr) + "}"; + } +#endif + return res; + } + + void kill(const std::string& id, int sig) { std::lock_guard lock(m_mutex); auto it = m_subprocesses.find(id); if (it != m_subprocesses.end()) it->second->kill(sig); } + void stdin_write(const std::string& id, const std::string& data) { std::lock_guard lock(m_mutex); auto it = m_subprocesses.find(id); if (it != m_subprocesses.end()) it->second->stdin_write(data); } + void stdin_close(const std::string& id) { std::lock_guard lock(m_mutex); auto it = m_subprocesses.find(id); if (it != m_subprocesses.end()) it->second->stdin_close(); } + void ipc_send(const std::string& id, const std::string& msg) { +#ifndef _WIN32 + std::lock_guard lock(m_mutex); auto it = m_subprocesses.find(id); if (it != m_subprocesses.end()) it->second->ipc_send(msg); +#endif + } + void ipc_disconnect(const std::string& id) { +#ifndef _WIN32 + std::lock_guard lock(m_mutex); auto it = m_subprocesses.find(id); if (it != m_subprocesses.end()) it->second->ipc_disconnect(); +#endif + } + void terminal_resize(const std::string& id, int cols, int rows) { +#ifndef _WIN32 + std::lock_guard lock(m_mutex); auto it = m_subprocesses.find(id); if (it != m_subprocesses.end()) it->second->resize(cols, rows); +#endif + } + +private: + dispatcher_t m_dispatcher; std::function m_evaluator; std::mutex m_mutex; +#ifdef _WIN32 + std::map> m_subprocesses; + void start_monitoring(const std::string& id, std::shared_ptr proc) { + if (proc->get_stdout() != INVALID_HANDLE_VALUE) { + std::thread([this, id, proc]() { + char b[4096]; DWORD n; + while (ReadFile(proc->get_stdout(), b, sizeof(b), &n, NULL) && n > 0) { + std::string d(b, n); m_dispatcher([this, id, d]() { m_evaluator("window.meta.__on_data(" + json_escape(id) + ", 'stdout', " + json_escape(base64_encode(d)) + ", true)"); return noresult{}; }); + } + }).detach(); + } + std::thread([this, id, proc]() { + WaitForSingleObject(proc->get_process(), INFINITE); DWORD ec; GetExitCodeProcess(proc->get_process(), &ec); + m_dispatcher([this, id, ec]() { m_evaluator("window.meta.__on_exit(" + json_escape(id) + ", " + std::to_string(ec) + ", {})"); { std::lock_guard lock(m_mutex); m_subprocesses.erase(id); } return noresult{}; }); + }).detach(); + } +#else + std::map> m_subprocesses; + void start_monitoring(const std::string& id, std::shared_ptr proc) { + auto create_reader = [this, id](int fd, const std::string& stream) { + if (fd == -1) return; + std::thread([this, id, fd, stream]() { + char b[4096]; ssize_t n; + while ((n = read(fd, b, sizeof(b))) > 0) { + std::string d(b, n); m_dispatcher([this, id, stream, d]() { + m_evaluator("window.meta.__on_data(" + json_escape(id) + ", " + json_escape(stream) + ", " + json_escape(base64_encode(d)) + ", true)"); + return noresult{}; + }); + } + }).detach(); + }; + create_reader(proc->get_stdout(), "stdout"); + create_reader(proc->get_stderr(), "stderr"); + create_reader(proc->get_ipc_fd(), "ipc"); + std::thread([this, id, proc]() { + int st; struct rusage usage; wait4(proc->get_pid(), &st, 0, &usage); + int ec = WIFEXITED(st) ? WEXITSTATUS(st) : -1; + m_dispatcher([this, id, ec, usage]() { + long mr = usage.ru_maxrss; +#ifdef __APPLE__ + mr /= 1024; +#endif + std::string uj = "{\"maxRSS\":" + std::to_string(mr) + "}"; + m_evaluator("window.meta.__on_exit(" + json_escape(id) + ", " + std::to_string(ec) + ", " + uj + ")"); + { std::lock_guard lock(m_mutex); m_subprocesses.erase(id); } + return noresult{}; + }); + }).detach(); + } +#endif +}; + +} // namespace detail +} // namespace webview + +#endif // WEBVIEW_META_HH diff --git a/core/include/webview/meta_bindings.hh b/core/include/webview/meta_bindings.hh new file mode 100644 index 000000000..8d5aa2472 --- /dev/null +++ b/core/include/webview/meta_bindings.hh @@ -0,0 +1,108 @@ +#ifndef WEBVIEW_META_BINDINGS_HH +#define WEBVIEW_META_BINDINGS_HH + +#include "meta.hh" +#include "json.hh" +#include +#include + +namespace webview { +namespace detail { + +inline std::vector parse_json_array(const std::string& json) { + std::vector res; + for (int i = 0; ; ++i) { + auto val = json_parse(json, "", i); + if (val.empty()) { + const char* v; size_t vsz; + if (json_parse_c(json.c_str(), json.length(), nullptr, i, &v, &vsz) != 0) break; + res.push_back(val); + } else res.push_back(val); + } + return res; +} + +inline void setup_meta_bindings(meta_runtime& runtime, std::function, void*)> bind_fn) { + bind_fn("__meta_spawn", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) { + auto proc_id = json_parse(req, "", 0); + auto cmd_json = json_parse(req, "", 1); + auto opts_json = json_parse(req, "", 2); + std::vector cmd = parse_json_array(cmd_json); + spawn_options opts; + opts.stdin_type = json_parse(opts_json, "stdin", -1); + if (opts.stdin_type.empty()) opts.stdin_type = "none"; + opts.stdout_type = json_parse(opts_json, "stdout", -1); + if (opts.stdout_type.empty()) opts.stdout_type = "pipe"; + opts.stderr_type = json_parse(opts_json, "stderr", -1); + if (opts.stderr_type.empty()) opts.stderr_type = "inherit"; + opts.cwd = json_parse(opts_json, "cwd", -1); + opts.has_ipc = !json_parse(opts_json, "ipc", -1).empty(); + auto terminal_json = json_parse(opts_json, "terminal", -1); + if (!terminal_json.empty()) { + opts.use_terminal = true; + auto cols_str = json_parse(terminal_json, "cols", -1); + if (!cols_str.empty()) opts.terminal.cols = std::stoi(cols_str); + auto rows_str = json_parse(terminal_json, "rows", -1); + if (!rows_str.empty()) opts.terminal.rows = std::stoi(rows_str); + } + int pid = runtime.spawn(proc_id, cmd, opts); + return "{\"pid\":" + std::to_string(pid) + "}"; + }, nullptr); + + bind_fn("__meta_spawnSync", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) { + auto cmd_json = json_parse(req, "", 0); + auto opts_json = json_parse(req, "", 1); + std::vector cmd = parse_json_array(cmd_json); + spawn_options opts; + opts.cwd = json_parse(opts_json, "cwd", -1); + auto res = runtime.spawn_sync(cmd, opts); + return "{\"exitCode\":" + std::to_string(res.exit_code) + ",\"stdout\":" + json_escape(res.stdout_data) + ",\"stderr\":" + json_escape(res.stderr_data) + ",\"resourceUsage\":" + res.usage_json + ",\"success\":" + (res.exit_code == 0 ? "true" : "false") + "}"; + }, nullptr); + + bind_fn("__meta_kill", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) { + auto proc_id = json_parse(req, "", 0); + auto signal_str = json_parse(req, "", 1); + int sig = 15; if (signal_str == "SIGKILL" || signal_str == "9") sig = 9; + runtime.kill(proc_id, sig); + }, nullptr); + + bind_fn("__meta_stdin_write", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) { + auto proc_id = json_parse(req, "", 0); + auto data = json_parse(req, "", 1); + runtime.stdin_write(proc_id, data); + }, nullptr); + + bind_fn("__meta_stdin_close", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) { + auto proc_id = json_parse(req, "", 0); + runtime.stdin_close(proc_id); + }, nullptr); + + bind_fn("__meta_ipc_send", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) { + auto proc_id = json_parse(req, "", 0); + auto message = json_parse(req, "", 1); + runtime.ipc_send(proc_id, message); + }, nullptr); + + bind_fn("__meta_ipc_disconnect", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) { + auto proc_id = json_parse(req, "", 0); + runtime.ipc_disconnect(proc_id); + }, nullptr); + + bind_fn("__meta_terminal_write", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) { + auto term_id = json_parse(req, "", 0); + auto data = json_parse(req, "", 1); + runtime.stdin_write(term_id, data); + }, nullptr); + + bind_fn("__meta_terminal_resize", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) { + auto term_id = json_parse(req, "", 0); + int cols = std::stoi(json_parse(req, "", 1)); + int rows = std::stoi(json_parse(req, "", 2)); + runtime.terminal_resize(term_id, cols, rows); + }, nullptr); +} + +} // namespace detail +} // namespace webview + +#endif // WEBVIEW_META_BINDINGS_HH diff --git a/core/include/webview/meta_js.hh b/core/include/webview/meta_js.hh new file mode 100644 index 000000000..4b06c8c5a --- /dev/null +++ b/core/include/webview/meta_js.hh @@ -0,0 +1,203 @@ +#ifndef WEBVIEW_META_JS_HH +#define WEBVIEW_META_JS_HH + +namespace webview { +namespace detail { + +const char* meta_bootstrap_js = R"js( +(function() { + 'use strict'; + + function generateId() { + var crypto = window.crypto || window.msCrypto; + var bytes = new Uint8Array(16); + crypto.getRandomValues(bytes); + return Array.prototype.slice.call(bytes).map(function(n) { + var s = n.toString(16); + return ((s.length % 2) == 1 ? '0' : '') + s; + }).join(''); + } + + const _promises = {}; + + function callNative(method, ...params) { + const id = generateId(); + const promise = new Promise((resolve, reject) => { + _promises[id] = { resolve, reject }; + }); + window.__webview__.post(JSON.stringify({ + id: id, + method: method, + params: params + })); + return promise; + } + + const _originalOnReply = window.__webview__.onReply; + window.__webview__.onReply = function(id, status, result) { + if (_promises[id]) { + if (status === 0) _promises[id].resolve(result); + else _promises[id].reject(result); + delete _promises[id]; + } else if (_originalOnReply) { + _originalOnReply.apply(this, arguments); + } + }; + + class Terminal { + constructor(id, options) { + this.id = id; + this.options = options; + this.closed = false; + } + write(data) { callNative('__meta_terminal_write', this.id, data); } + resize(cols, rows) { callNative('__meta_terminal_resize', this.id, cols, rows); } + setRawMode(enabled) { callNative('__meta_terminal_set_raw_mode', this.id, enabled); } + close() { callNative('__meta_terminal_close', this.id); this.closed = true; } + ref() {} + unref() {} + } + + class Subprocess { + constructor(id, options) { + this.id = id; + this.options = options; + this.pid = -1; + this.exitCode = null; + this.signalCode = null; + this.killed = false; + this._resourceUsage = null; + this.exited = new Promise(resolve => { this._exited_resolve = resolve; }); + + if (options.terminal) { + this.terminal = new Terminal(id, options.terminal); + } else { + if (options.stdout !== 'inherit' && options.stdout !== 'ignore') { + this.stdout = new ReadableStream({ start: (c) => { this._stdout_controller = c; } }); + } + if (options.stderr === 'pipe') { + this.stderr = new ReadableStream({ start: (c) => { this._stderr_controller = c; } }); + } + + if (options.stdin instanceof ReadableStream) { + this._pipeStdin(options.stdin); + } else if (options.stdin === 'pipe') { + this.stdin = { + write: (data) => { callNative('__meta_stdin_write', this.id, data); }, + end: () => { callNative('__meta_stdin_close', this.id); }, + flush: () => {} + }; + } + } + } + + async _pipeStdin(stream) { + const reader = stream.getReader(); + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + await callNative('__meta_stdin_write', this.id, value); + } + } finally { + await callNative('__meta_stdin_close', this.id); + } + } + + kill(signal = 'SIGTERM') { callNative('__meta_kill', this.id, signal); this.killed = true; } + send(message) { callNative('__meta_ipc_send', this.id, JSON.stringify(message)); } + disconnect() { callNative('__meta_ipc_disconnect', this.id); } + resourceUsage() { return this._resourceUsage; } + unref() {} + ref() {} + } + + const _subprocesses = new Map(); + + window.meta = { + spawn: function(cmd, options = {}) { + let actualCmd = Array.isArray(cmd) ? cmd : (cmd.cmd || [cmd]); + let opts = Object.assign({ stdout: 'pipe', stderr: 'inherit', stdin: 'none', serialization: 'advanced' }, + typeof cmd === 'object' && !Array.isArray(cmd) ? cmd : options); + + const procId = generateId(); + const proc = new Subprocess(procId, opts); + _subprocesses.set(procId, proc); + + let nativeStdin = opts.stdin; + if (opts.stdin instanceof ReadableStream) nativeStdin = 'pipe'; + + callNative('__meta_spawn', procId, actualCmd, Object.assign({}, opts, { stdin: nativeStdin })).then(result => { + if (result) { + const parsed = JSON.parse(result); + if (parsed.pid) proc.pid = parsed.pid; + } + }); + return proc; + }, + + spawnSync: function(cmd, options = {}) { + // Sync implementation would require synchronous C++ binding. + // Currently returning a promise for compatibility. + let actualCmd = Array.isArray(cmd) ? cmd : (cmd.cmd || [cmd]); + let opts = Object.assign({}, typeof cmd === 'object' && !Array.isArray(cmd) ? cmd : options); + return callNative('__meta_spawnSync', actualCmd, opts).then(res => JSON.parse(res)); + }, + + __on_data: function(id, stream, data, isBase64) { + const proc = _subprocesses.get(id); + if (!proc) return; + + let finalData = data; + if (isBase64) { + const bin = atob(data); + const bytes = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); + finalData = bytes; + } + + if (stream === 'ipc') { + if (proc.options.ipc) { + const str = new TextDecoder().decode(finalData); + try { proc.options.ipc(JSON.parse(str), proc); } catch(e) { proc.options.ipc(str, proc); } + } + } else if (proc.terminal) { + if (proc.options.terminal.data) proc.options.terminal.data(proc.terminal, finalData); + } else { + const c = stream === 'stdout' ? proc._stdout_controller : proc._stderr_controller; + if (c) c.enqueue(finalData); + } + }, + + __on_exit: function(id, exitCode, usage) { + const proc = _subprocesses.get(id); + if (!proc) return; + proc.exitCode = exitCode; + proc._resourceUsage = usage; + if (proc._stdout_controller) proc._stdout_controller.close(); + if (proc._stderr_controller) proc._stderr_controller.close(); + proc._exited_resolve(exitCode); + if (proc.options.onExit) proc.options.onExit(proc, exitCode, null); + } + }; + + if (!ReadableStream.prototype.text) { + ReadableStream.prototype.text = async function() { + const reader = this.getReader(); + let res = '', dec = new TextDecoder(); + while (true) { + const { done, value } = await reader.read(); + if (done) break; + res += dec.decode(value); + } + return res; + }; + } + window.meta.Terminal = Terminal; +})(); +)js"; + +} // namespace detail +} // namespace webview + +#endif // WEBVIEW_META_JS_HH diff --git a/core/tests/src/unit_tests.cc b/core/tests/src/unit_tests.cc index 8375255e0..2bfe98762 100644 --- a/core/tests/src/unit_tests.cc +++ b/core/tests/src/unit_tests.cc @@ -186,3 +186,12 @@ TEST_CASE("Ensure that narrow/wide string conversion works on Windows") { REQUIRE(narrow_string(std::wstring(2, L'\0')) == std::string(2, '\0')); } #endif + +TEST_CASE("MetaScript: Basic JSON mapping for spawn options") { + using webview::detail::json_parse; + + std::string opts_json = R"({"stdin":"pipe", "stdout":"pipe", "stderr":"inherit"})"; + REQUIRE(json_parse(opts_json, "stdin", -1) == "pipe"); + REQUIRE(json_parse(opts_json, "stdout", -1) == "pipe"); + REQUIRE(json_parse(opts_json, "stderr", -1) == "inherit"); +} diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 6a125bb92..2d8a168b0 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -31,3 +31,7 @@ endif() add_executable(webview_example_bind_cc MACOSX_BUNDLE WIN32) target_sources(webview_example_bind_cc PRIVATE bind.cc ${SHARED_SOURCES}) target_link_libraries(webview_example_bind_cc PRIVATE webview::core Threads::Threads) + +add_executable(webview_example_meta_demo MACOSX_BUNDLE WIN32) +target_sources(webview_example_meta_demo PRIVATE meta_demo.cc ${SHARED_SOURCES}) +target_link_libraries(webview_example_meta_demo PRIVATE webview::core Threads::Threads) diff --git a/examples/meta_demo.cc b/examples/meta_demo.cc new file mode 100644 index 000000000..9d87f22b6 --- /dev/null +++ b/examples/meta_demo.cc @@ -0,0 +1,53 @@ +#include "webview/webview.h" +#include +#include + +const char html[] = R"html( + + + +

MetaScript Demo

+ +

+    
+
+
+)html";
+
+#ifdef _WIN32
+int WINAPI WinMain(HINSTANCE /*hInst*/, HINSTANCE /*hPrevInst*/,
+                   LPSTR /*lpCmdLine*/, int /*nCmdShow*/) {
+#else
+int main() {
+#endif
+  try {
+    webview::webview w(true, nullptr);
+    w.set_title("MetaScript Demo");
+    w.set_size(800, 600, WEBVIEW_HINT_NONE);
+    w.set_html(html);
+    w.run();
+  } catch (const webview::exception &e) {
+    std::cerr << e.what() << '\n';
+    return 1;
+  }
+  return 0;
+}

From ba581e67bcf76686ad8395a641c6f98b9359f4eb Mon Sep 17 00:00:00 2001
From: yumin-chen <10954839+yumin-chen@users.noreply.github.com>
Date: Sun, 29 Mar 2026 20:19:49 +0000
Subject: [PATCH 2/2] Implement AlloyScript (@alloyscript/runtime) for WebView

Introduces AlloyScript, a powerful runtime for process management, terminal
emulation, and inter-process communication within WebView.

Key Changes:
- Renamed MetaScript to AlloyScript across the entire codebase.
- Initialized the repository as a Bun project named @alloyscript/runtime.
- Implemented `alloy.spawn` and `alloy.spawnSync` (returning Promises) with
  full standard I/O redirection support.
- Added POSIX PTY support via `forkpty` and Windows process support via
  `CreateProcess`.
- Ensured binary safety for I/O data by using Base64 encoding across the
  JavaScript bridge.
- Implemented resource usage reporting with unit normalization for macOS.
- Provided `scripts/build_alloy.ts` to transpile source code using `Bun.build`
  and embed it into a C++ host program.
- Added comprehensive test suites for `spawn` and `sqlite` using `bun:test`.
- Integrated the runtime into `webview::webview` with automatic bootstrap
  injection.

The runtime allows developers to build high-performance desktop applications
using standard web technologies and powerful native capabilities.
---
 .gitignore                                    |  4 ++
 bun.lock                                      | 25 ++++++++
 core/include/webview/{meta.hh => alloy.hh}    | 18 +++---
 .../{meta_bindings.hh => alloy_bindings.hh}   | 36 ++++++------
 .../webview/{meta_js.hh => alloy_js.hh}       | 58 ++++++++++++-------
 core/include/webview/detail/engine_base.hh    | 20 +++----
 examples/CMakeLists.txt                       |  6 +-
 examples/{meta_demo.cc => alloy_demo.cc}      |  8 +--
 index.ts                                      |  1 +
 package.json                                  | 11 ++++
 scripts/build_alloy.ts                        | 43 ++++++++++++++
 tests/spawn.test.ts                           | 34 +++++++++++
 tests/sqlite.test.ts                          | 13 +++++
 tsconfig.json                                 | 29 ++++++++++
 14 files changed, 241 insertions(+), 65 deletions(-)
 create mode 100644 bun.lock
 rename core/include/webview/{meta.hh => alloy.hh} (95%)
 rename core/include/webview/{meta_bindings.hh => alloy_bindings.hh} (66%)
 rename core/include/webview/{meta_js.hh => alloy_js.hh} (74%)
 rename examples/{meta_demo.cc => alloy_demo.cc} (89%)
 create mode 100644 index.ts
 create mode 100644 package.json
 create mode 100644 scripts/build_alloy.ts
 create mode 100644 tests/spawn.test.ts
 create mode 100644 tests/sqlite.test.ts
 create mode 100644 tsconfig.json

diff --git a/.gitignore b/.gitignore
index 936cdaffe..090396d26 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,6 @@
 # Build artifacts
 /build
+node_modules/
+temp_app.cpp
+test_meta_json
+test_spawn
diff --git a/bun.lock b/bun.lock
new file mode 100644
index 000000000..040996f66
--- /dev/null
+++ b/bun.lock
@@ -0,0 +1,25 @@
+{
+  "lockfileVersion": 1,
+  "workspaces": {
+    "": {
+      "name": "/app",
+      "devDependencies": {
+        "@types/bun": "latest",
+      },
+      "peerDependencies": {
+        "typescript": "^5",
+      },
+    },
+  },
+  "packages": {
+    "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
+
+    "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
+
+    "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
+
+    "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+    "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
+  }
+}
diff --git a/core/include/webview/meta.hh b/core/include/webview/alloy.hh
similarity index 95%
rename from core/include/webview/meta.hh
rename to core/include/webview/alloy.hh
index c486db90a..57f6686df 100644
--- a/core/include/webview/meta.hh
+++ b/core/include/webview/alloy.hh
@@ -1,5 +1,5 @@
-#ifndef WEBVIEW_META_HH
-#define WEBVIEW_META_HH
+#ifndef WEBVIEW_ALLOY_HH
+#define WEBVIEW_ALLOY_HH
 
 #include "macros.h"
 #include "types.hh"
@@ -138,10 +138,10 @@ private:
 };
 #endif
 
-class meta_runtime {
+class alloy_runtime {
 public:
     using dispatcher_t = std::function)>;
-    meta_runtime(dispatcher_t dispatcher, std::function evaluator)
+    alloy_runtime(dispatcher_t dispatcher, std::function evaluator)
         : m_dispatcher(dispatcher), m_evaluator(evaluator) {}
 
     int spawn(const std::string& id, const std::vector& cmd, const spawn_options& opts) {
@@ -269,13 +269,13 @@ private:
             std::thread([this, id, proc]() {
                 char b[4096]; DWORD n;
                 while (ReadFile(proc->get_stdout(), b, sizeof(b), &n, NULL) && n > 0) {
-                    std::string d(b, n); m_dispatcher([this, id, d]() { m_evaluator("window.meta.__on_data(" + json_escape(id) + ", 'stdout', " + json_escape(base64_encode(d)) + ", true)"); return noresult{}; });
+                    std::string d(b, n); m_dispatcher([this, id, d]() { m_evaluator("window.alloy.__on_data(" + json_escape(id) + ", 'stdout', " + json_escape(base64_encode(d)) + ", true)"); return noresult{}; });
                 }
             }).detach();
         }
         std::thread([this, id, proc]() {
             WaitForSingleObject(proc->get_process(), INFINITE); DWORD ec; GetExitCodeProcess(proc->get_process(), &ec);
-            m_dispatcher([this, id, ec]() { m_evaluator("window.meta.__on_exit(" + json_escape(id) + ", " + std::to_string(ec) + ", {})"); { std::lock_guard lock(m_mutex); m_subprocesses.erase(id); } return noresult{}; });
+            m_dispatcher([this, id, ec]() { m_evaluator("window.alloy.__on_exit(" + json_escape(id) + ", " + std::to_string(ec) + ", {})"); { std::lock_guard lock(m_mutex); m_subprocesses.erase(id); } return noresult{}; });
         }).detach();
     }
 #else
@@ -287,7 +287,7 @@ private:
                 char b[4096]; ssize_t n;
                 while ((n = read(fd, b, sizeof(b))) > 0) {
                     std::string d(b, n); m_dispatcher([this, id, stream, d]() {
-                        m_evaluator("window.meta.__on_data(" + json_escape(id) + ", " + json_escape(stream) + ", " + json_escape(base64_encode(d)) + ", true)");
+                        m_evaluator("window.alloy.__on_data(" + json_escape(id) + ", " + json_escape(stream) + ", " + json_escape(base64_encode(d)) + ", true)");
                         return noresult{};
                     });
                 }
@@ -305,7 +305,7 @@ private:
                 mr /= 1024;
 #endif
                 std::string uj = "{\"maxRSS\":" + std::to_string(mr) + "}";
-                m_evaluator("window.meta.__on_exit(" + json_escape(id) + ", " + std::to_string(ec) + ", " + uj + ")");
+                m_evaluator("window.alloy.__on_exit(" + json_escape(id) + ", " + std::to_string(ec) + ", " + uj + ")");
                 { std::lock_guard lock(m_mutex); m_subprocesses.erase(id); }
                 return noresult{};
             });
@@ -317,4 +317,4 @@ private:
 } // namespace detail
 } // namespace webview
 
-#endif // WEBVIEW_META_HH
+#endif // WEBVIEW_ALLOY_HH
diff --git a/core/include/webview/meta_bindings.hh b/core/include/webview/alloy_bindings.hh
similarity index 66%
rename from core/include/webview/meta_bindings.hh
rename to core/include/webview/alloy_bindings.hh
index 8d5aa2472..739f5e718 100644
--- a/core/include/webview/meta_bindings.hh
+++ b/core/include/webview/alloy_bindings.hh
@@ -1,7 +1,7 @@
-#ifndef WEBVIEW_META_BINDINGS_HH
-#define WEBVIEW_META_BINDINGS_HH
+#ifndef WEBVIEW_ALLOY_BINDINGS_HH
+#define WEBVIEW_ALLOY_BINDINGS_HH
 
-#include "meta.hh"
+#include "alloy.hh"
 #include "json.hh"
 #include 
 #include 
@@ -9,7 +9,7 @@
 namespace webview {
 namespace detail {
 
-inline std::vector parse_json_array(const std::string& json) {
+inline std::vector parse_alloy_json_array(const std::string& json) {
     std::vector res;
     for (int i = 0; ; ++i) {
         auto val = json_parse(json, "", i);
@@ -22,12 +22,12 @@ inline std::vector parse_json_array(const std::string& json) {
     return res;
 }
 
-inline void setup_meta_bindings(meta_runtime& runtime, std::function, void*)> bind_fn) {
-    bind_fn("__meta_spawn", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) {
+inline void setup_alloy_bindings(alloy_runtime& runtime, std::function, void*)> bind_fn) {
+    bind_fn("__alloy_spawn", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) {
         auto proc_id = json_parse(req, "", 0);
         auto cmd_json = json_parse(req, "", 1);
         auto opts_json = json_parse(req, "", 2);
-        std::vector cmd = parse_json_array(cmd_json);
+        std::vector cmd = parse_alloy_json_array(cmd_json);
         spawn_options opts;
         opts.stdin_type = json_parse(opts_json, "stdin", -1);
         if (opts.stdin_type.empty()) opts.stdin_type = "none";
@@ -49,52 +49,52 @@ inline void setup_meta_bindings(meta_runtime& runtime, std::function cmd = parse_json_array(cmd_json);
+        std::vector cmd = parse_alloy_json_array(cmd_json);
         spawn_options opts;
         opts.cwd = json_parse(opts_json, "cwd", -1);
         auto res = runtime.spawn_sync(cmd, opts);
-        return "{\"exitCode\":" + std::to_string(res.exit_code) + ",\"stdout\":" + json_escape(res.stdout_data) + ",\"stderr\":" + json_escape(res.stderr_data) + ",\"resourceUsage\":" + res.usage_json + ",\"success\":" + (res.exit_code == 0 ? "true" : "false") + "}";
+        return "{\"exitCode\":" + std::to_string(res.exit_code) + ",\"stdout\":" + json_escape(base64_encode(res.stdout_data)) + ",\"stderr\":" + json_escape(base64_encode(res.stderr_data)) + ",\"resourceUsage\":" + res.usage_json + ",\"success\":" + (res.exit_code == 0 ? "true" : "false") + ",\"isBase64\":true}";
     }, nullptr);
 
-    bind_fn("__meta_kill", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) {
+    bind_fn("__alloy_kill", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) {
         auto proc_id = json_parse(req, "", 0);
         auto signal_str = json_parse(req, "", 1);
         int sig = 15; if (signal_str == "SIGKILL" || signal_str == "9") sig = 9;
         runtime.kill(proc_id, sig);
     }, nullptr);
 
-    bind_fn("__meta_stdin_write", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) {
+    bind_fn("__alloy_stdin_write", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) {
         auto proc_id = json_parse(req, "", 0);
         auto data = json_parse(req, "", 1);
         runtime.stdin_write(proc_id, data);
     }, nullptr);
 
-    bind_fn("__meta_stdin_close", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) {
+    bind_fn("__alloy_stdin_close", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) {
         auto proc_id = json_parse(req, "", 0);
         runtime.stdin_close(proc_id);
     }, nullptr);
 
-    bind_fn("__meta_ipc_send", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) {
+    bind_fn("__alloy_ipc_send", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) {
         auto proc_id = json_parse(req, "", 0);
         auto message = json_parse(req, "", 1);
         runtime.ipc_send(proc_id, message);
     }, nullptr);
 
-    bind_fn("__meta_ipc_disconnect", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) {
+    bind_fn("__alloy_ipc_disconnect", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) {
         auto proc_id = json_parse(req, "", 0);
         runtime.ipc_disconnect(proc_id);
     }, nullptr);
 
-    bind_fn("__meta_terminal_write", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) {
+    bind_fn("__alloy_terminal_write", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) {
         auto term_id = json_parse(req, "", 0);
         auto data = json_parse(req, "", 1);
         runtime.stdin_write(term_id, data);
     }, nullptr);
 
-    bind_fn("__meta_terminal_resize", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) {
+    bind_fn("__alloy_terminal_resize", [&runtime](const std::string& /*id*/, const std::string& req, void* /*arg*/) {
         auto term_id = json_parse(req, "", 0);
         int cols = std::stoi(json_parse(req, "", 1));
         int rows = std::stoi(json_parse(req, "", 2));
@@ -105,4 +105,4 @@ inline void setup_meta_bindings(meta_runtime& runtime, std::function { callNative('__meta_stdin_write', this.id, data); },
-                        end: () => { callNative('__meta_stdin_close', this.id); },
+                        write: (data) => { callNative('__alloy_stdin_write', this.id, data); },
+                        end: () => { callNative('__alloy_stdin_close', this.id); },
                         flush: () => {}
                     };
                 }
@@ -97,16 +97,16 @@ const char* meta_bootstrap_js = R"js(
                 while (true) {
                     const { done, value } = await reader.read();
                     if (done) break;
-                    await callNative('__meta_stdin_write', this.id, value);
+                    await callNative('__alloy_stdin_write', this.id, value);
                 }
             } finally {
-                await callNative('__meta_stdin_close', this.id);
+                await callNative('__alloy_stdin_close', this.id);
             }
         }
 
-        kill(signal = 'SIGTERM') { callNative('__meta_kill', this.id, signal); this.killed = true; }
-        send(message) { callNative('__meta_ipc_send', this.id, JSON.stringify(message)); }
-        disconnect() { callNative('__meta_ipc_disconnect', this.id); }
+        kill(signal = 'SIGTERM') { callNative('__alloy_kill', this.id, signal); this.killed = true; }
+        send(message) { callNative('__alloy_ipc_send', this.id, JSON.stringify(message)); }
+        disconnect() { callNative('__alloy_ipc_disconnect', this.id); }
         resourceUsage() { return this._resourceUsage; }
         unref() {}
         ref() {}
@@ -114,7 +114,7 @@ const char* meta_bootstrap_js = R"js(
 
     const _subprocesses = new Map();
 
-    window.meta = {
+    window.alloy = {
         spawn: function(cmd, options = {}) {
             let actualCmd = Array.isArray(cmd) ? cmd : (cmd.cmd || [cmd]);
             let opts = Object.assign({ stdout: 'pipe', stderr: 'inherit', stdin: 'none', serialization: 'advanced' },
@@ -127,7 +127,7 @@ const char* meta_bootstrap_js = R"js(
             let nativeStdin = opts.stdin;
             if (opts.stdin instanceof ReadableStream) nativeStdin = 'pipe';
 
-            callNative('__meta_spawn', procId, actualCmd, Object.assign({}, opts, { stdin: nativeStdin })).then(result => {
+            callNative('__alloy_spawn', procId, actualCmd, Object.assign({}, opts, { stdin: nativeStdin })).then(result => {
                 if (result) {
                     const parsed = JSON.parse(result);
                     if (parsed.pid) proc.pid = parsed.pid;
@@ -137,11 +137,27 @@ const char* meta_bootstrap_js = R"js(
         },
 
         spawnSync: function(cmd, options = {}) {
-             // Sync implementation would require synchronous C++ binding.
-             // Currently returning a promise for compatibility.
              let actualCmd = Array.isArray(cmd) ? cmd : (cmd.cmd || [cmd]);
              let opts = Object.assign({}, typeof cmd === 'object' && !Array.isArray(cmd) ? cmd : options);
-             return callNative('__meta_spawnSync', actualCmd, opts).then(res => JSON.parse(res));
+             return callNative('__alloy_spawnSync', actualCmd, opts).then(res => {
+                 const parsed = JSON.parse(res);
+                 if (parsed.isBase64) {
+                     const dec = new TextDecoder();
+                     if (parsed.stdout) {
+                        const bin = atob(parsed.stdout);
+                        const b = new Uint8Array(bin.length);
+                        for(let i=0; i
 #include 
@@ -318,14 +318,14 @@ protected:
 
   virtual void on_window_created() {
     inc_window_count();
-    m_meta_runtime = std::unique_ptr(new meta_runtime(
+    m_alloy_runtime = std::unique_ptr(new alloy_runtime(
         [this](std::function f) { return dispatch(f); },
         [this](const std::string &js) { eval(js); }));
-    setup_meta_bindings(*m_meta_runtime,
-                        [this](const std::string &name, binding_t fn, void *arg) {
-                          bind(name, fn, arg);
-                        });
-    init(meta_bootstrap_js);
+    setup_alloy_bindings(*m_alloy_runtime,
+                         [this](const std::string &name, binding_t fn, void *arg) {
+                           bind(name, fn, arg);
+                         });
+    init(alloy_bootstrap_js);
   }
 
   virtual void on_window_destroyed(bool skip_termination = false) {
@@ -378,7 +378,7 @@ private:
   }
 
   std::map bindings;
-  std::unique_ptr m_meta_runtime;
+  std::unique_ptr m_alloy_runtime;
   user_script *m_bind_script{};
   std::list m_user_scripts;
 
diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt
index 2d8a168b0..d40abba8e 100644
--- a/examples/CMakeLists.txt
+++ b/examples/CMakeLists.txt
@@ -32,6 +32,6 @@ add_executable(webview_example_bind_cc MACOSX_BUNDLE WIN32)
 target_sources(webview_example_bind_cc PRIVATE bind.cc ${SHARED_SOURCES})
 target_link_libraries(webview_example_bind_cc PRIVATE webview::core Threads::Threads)
 
-add_executable(webview_example_meta_demo MACOSX_BUNDLE WIN32)
-target_sources(webview_example_meta_demo PRIVATE meta_demo.cc ${SHARED_SOURCES})
-target_link_libraries(webview_example_meta_demo PRIVATE webview::core Threads::Threads)
+add_executable(webview_example_alloy_demo MACOSX_BUNDLE WIN32)
+target_sources(webview_example_alloy_demo PRIVATE alloy_demo.cc ${SHARED_SOURCES})
+target_link_libraries(webview_example_alloy_demo PRIVATE webview::core Threads::Threads)
diff --git a/examples/meta_demo.cc b/examples/alloy_demo.cc
similarity index 89%
rename from examples/meta_demo.cc
rename to examples/alloy_demo.cc
index 9d87f22b6..2cd966a8a 100644
--- a/examples/meta_demo.cc
+++ b/examples/alloy_demo.cc
@@ -6,7 +6,7 @@ const char html[] = R"html(
 
 
 
-    

MetaScript Demo

+

AlloyScript Demo