| #ifdef WITH_RIVE_SCRIPTING_WASM | |
| #include "rive/wasm/module_tier_ladder.hpp" | |
| #include "rive/wasm/aot_artifact.hpp" | |
| #include "rive/wasm/wasm_lane.hpp" | |
| #include <cstdlib> | |
| #include <cstring> | |
| namespace rive | |
| { | |
| TierSpecies bootTierSpecies() | |
| { | |
| const char* tier = getenv("RIVE_WASM_AOT_SYNC"); | |
| if (tier != nullptr && strcmp(tier, "o0") == 0) | |
| { | |
| return TierSpecies::o0; | |
| } | |
| #ifdef RIVE_WASM_HW_BOUNDS | |
| return TierSpecies::hw; | |
| #else | |
| return TierSpecies::o3; | |
| #endif | |
| } | |
| std::vector<TierSpecies> aotTargetSpecies( | |
| const std::string& target, | |
| bool hardwareBounds) | |
| { | |
| if (hardwareBounds && target == kAotTargetAndroidAarch64) | |
| { | |
| return {TierSpecies::o3, TierSpecies::hw}; | |
| } | |
| return {TierSpecies::o3}; | |
| } | |
| int x86_64Level( | |
| uint32_t leaf1Ecx, | |
| uint32_t leaf7Ebx, | |
| uint32_t extEcx, | |
| uint64_t xcr0) | |
| { | |
| // FMA, MOVBE, XSAVE, OSXSAVE, AVX and F16C, then BMI1, AVX2 and BMI2, then | |
| // LZCNT, then the XMM and YMM state. | |
| constexpr uint32_t kLeaf1 = (1u << 12) | (1u << 22) | (1u << 26) | | |
| (1u << 27) | (1u << 28) | (1u << 29); | |
| constexpr uint32_t kLeaf7 = (1u << 3) | (1u << 5) | (1u << 8); | |
| constexpr uint32_t kExt = 1u << 5; | |
| constexpr uint64_t kXcr0 = 0x6; | |
| return (leaf1Ecx & kLeaf1) == kLeaf1 && (leaf7Ebx & kLeaf7) == kLeaf7 && | |
| (extEcx & kExt) == kExt && (xcr0 & kXcr0) == kXcr0 | |
| ? 3 | |
| : 2; | |
| } | |
| } // namespace rive | |
| #ifdef RIVE_WASM_TIER_LADDER | |
| #include "wasm_export.h" | |
| #include <algorithm> | |
| #include <atomic> | |
| #include <cerrno> | |
| #include <chrono> | |
| #include <cstdio> | |
| #include <filesystem> | |
| #include <fstream> | |
| #include <iterator> | |
| #include <cstdlib> | |
| #include <cstring> | |
| #include <thread> | |
| #include <sys/stat.h> | |
| #ifdef _WIN32 | |
| #include <windows.h> | |
| #else | |
| #include <fcntl.h> | |
| #include <signal.h> | |
| #include <spawn.h> | |
| #include <sys/resource.h> | |
| #include <sys/wait.h> | |
| #include <unistd.h> | |
| #if !defined(RIVE_ANDROID) && !defined(RIVE_NX) | |
| #include <dlfcn.h> | |
| #endif | |
| #if defined(__linux__) && !defined(__ANDROID__) | |
| #include <link.h> | |
| #endif | |
| extern char** environ; | |
| #endif | |
| #if defined(__x86_64__) || defined(_M_X64) | |
| #if defined(_MSC_VER) && !defined(__clang__) | |
| #include <intrin.h> | |
| #else | |
| #include <cpuid.h> | |
| #endif | |
| #endif | |
| namespace rive | |
| { | |
| constexpr const char* kCancelledReason = "the compile was cancelled"; | |
| static uint64_t processId() | |
| { | |
| #ifdef _WIN32 | |
| return GetCurrentProcessId(); | |
| #else | |
| return (uint64_t)getpid(); | |
| #endif | |
| } | |
| static void makeDir(const std::string& path) | |
| { | |
| #ifdef RIVE_NX | |
| // Our NX libc++ has no std::filesystem directory ops. | |
| mkdir(path.c_str(), 0755); | |
| #elif defined(_WIN32) | |
| std::error_code ec; | |
| std::filesystem::create_directories(path, ec); | |
| #else | |
| // Private from the start, so no other user can plant artifacts. | |
| if (mkdir(path.c_str(), 0700) != 0 && errno == ENOENT) | |
| { | |
| size_t slash = path.find_last_of('/'); | |
| if (slash != std::string::npos && slash != 0) | |
| { | |
| makeDir(path.substr(0, slash)); | |
| mkdir(path.c_str(), 0700); | |
| } | |
| } | |
| #endif | |
| } | |
| static bool fileExists(const std::string& path) | |
| { | |
| struct stat st; | |
| return stat(path.c_str(), &st) == 0; | |
| } | |
| // Artifacts run as native code, so a cache another user can write is never | |
| // read. One that does not exist yet gets created private. | |
| static bool trustedCacheDir(const std::string& path) | |
| { | |
| #if defined(_WIN32) || defined(RIVE_NX) | |
| return true; | |
| #else | |
| struct stat st; | |
| return stat(path.c_str(), &st) != 0 || | |
| (S_ISDIR(st.st_mode) && st.st_uid == geteuid() && | |
| (st.st_mode & (S_IWGRP | S_IWOTH)) == 0); | |
| #endif | |
| } | |
| #ifndef RIVE_NX | |
| // The temp dir is per user on Apple and Windows but shared on Linux, where | |
| // the user's own cache dir holds the artifacts instead. | |
| static std::string defaultCacheDir() | |
| { | |
| #if defined(_WIN32) || defined(__APPLE__) | |
| return (std::filesystem::temp_directory_path() / "rive_aot_cache").string(); | |
| #else | |
| const char* xdg = getenv("XDG_CACHE_HOME"); | |
| if (xdg != nullptr && xdg[0] == '/') | |
| { | |
| return std::string(xdg) + "/rive/aot_cache"; | |
| } | |
| const char* home = getenv("HOME"); | |
| return home != nullptr && home[0] == '/' | |
| ? std::string(home) + "/.cache/rive/aot_cache" | |
| : std::string(); | |
| #endif | |
| } | |
| #endif | |
| // Windows refuses to rename over an existing file, and another compile of | |
| // the same artifact landing first is still a success. | |
| static bool publishFile(const std::string& tmpPath, const std::string& path) | |
| { | |
| if (rename(tmpPath.c_str(), path.c_str()) == 0) | |
| { | |
| return true; | |
| } | |
| std::remove(tmpPath.c_str()); | |
| return fileExists(path); | |
| } | |
| // The x86-64 level this machine's host artifacts compile for, 0 off x86. | |
| static int hostX86_64Level() | |
| { | |
| #if defined(__x86_64__) || defined(_M_X64) | |
| static const int level = [] { | |
| // CI holds a v3 host at the baseline, where relaxed madd calls fmaf. | |
| const char* cap = getenv("RIVE_AOT_X86_LEVEL"); | |
| if (cap != nullptr && strcmp(cap, "2") == 0) | |
| { | |
| return 2; | |
| } | |
| uint32_t leaf1Ecx = 0; | |
| uint32_t leaf7Ebx = 0; | |
| uint32_t extEcx = 0; | |
| uint64_t xcr0 = 0; | |
| #if defined(_MSC_VER) && !defined(__clang__) | |
| int regs[4]; | |
| __cpuid(regs, 0); | |
| int maxLeaf = regs[0]; | |
| __cpuid(regs, 1); | |
| leaf1Ecx = (uint32_t)regs[2]; | |
| if (maxLeaf >= 7) | |
| { | |
| __cpuidex(regs, 7, 0); | |
| leaf7Ebx = (uint32_t)regs[1]; | |
| } | |
| __cpuid(regs, 0x80000000); | |
| if ((uint32_t)regs[0] >= 0x80000001) | |
| { | |
| __cpuid(regs, 0x80000001); | |
| extEcx = (uint32_t)regs[2]; | |
| } | |
| if ((leaf1Ecx & (1u << 27)) != 0) | |
| { | |
| xcr0 = _xgetbv(0); | |
| } | |
| #else | |
| unsigned a, b, c, d; | |
| if (__get_cpuid(1, &a, &b, &c, &d)) | |
| { | |
| leaf1Ecx = c; | |
| } | |
| if (__get_cpuid_count(7, 0, &a, &b, &c, &d)) | |
| { | |
| leaf7Ebx = b; | |
| } | |
| if (__get_cpuid(0x80000001, &a, &b, &c, &d)) | |
| { | |
| extEcx = c; | |
| } | |
| // xgetbv faults unless the OS turned on xsave. | |
| if ((leaf1Ecx & (1u << 27)) != 0) | |
| { | |
| uint32_t lo; | |
| uint32_t hi; | |
| __asm__ volatile("xgetbv" : "=a"(lo), "=d"(hi) : "c"(0)); | |
| xcr0 = ((uint64_t)hi << 32) | lo; | |
| } | |
| #endif | |
| return x86_64Level(leaf1Ecx, leaf7Ebx, extEcx, xcr0); | |
| }(); | |
| return level; | |
| #else | |
| return 0; | |
| #endif | |
| } | |
| std::string wamrcHwGuardFlag() | |
| { | |
| return "--rive-hw-guard=" + std::to_string(WASM_RIVE_HW_GUARD_SIZE); | |
| } | |
| std::string loaderBinaryPath() | |
| { | |
| std::string path; | |
| #if defined(_WIN32) | |
| HMODULE module = nullptr; | |
| char name[MAX_PATH]; | |
| if (GetModuleHandleExA( | |
| GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | | |
| GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, | |
| (LPCSTR)&loaderBinaryPath, | |
| &module)) | |
| { | |
| DWORD length = GetModuleFileNameA(module, name, MAX_PATH); | |
| if (length > 0 && length < MAX_PATH) | |
| { | |
| path.assign(name, length); | |
| } | |
| } | |
| #elif defined(__linux__) && !defined(__ANDROID__) | |
| Dl_info info; | |
| link_map* map = nullptr; | |
| if (dladdr1( | |
| (const void*)&loaderBinaryPath, | |
| &info, | |
| (void**)&map, | |
| RTLD_DL_LINKMAP) != 0 && | |
| map != nullptr) | |
| { | |
| // glibc names the main program by argv[0], bare on a PATH launch. | |
| path = map->l_name[0] == '\0' ? "/proc/self/exe" : map->l_name; | |
| } | |
| #elif !defined(RIVE_ANDROID) && !defined(RIVE_NX) | |
| Dl_info info; | |
| if (dladdr((const void*)&loaderBinaryPath, &info) != 0 && | |
| info.dli_fname != nullptr) | |
| { | |
| path = info.dli_fname; | |
| } | |
| #endif | |
| return path; | |
| } | |
| // Every relink rewrites the binary holding the loader, so its stat tells | |
| // builds apart without reading it. | |
| static const std::string& loaderBuild() | |
| { | |
| static const std::string build = [] { | |
| std::string path = loaderBinaryPath(); | |
| struct stat st; | |
| uint64_t key = 0; | |
| if (!path.empty() && stat(path.c_str(), &st) == 0) | |
| { | |
| const uint64_t fields[] = { | |
| (uint64_t)st.st_ino, | |
| (uint64_t)st.st_size, | |
| (uint64_t)st.st_mtime}; | |
| key = wasmModuleKey( | |
| Span<const uint8_t>((const uint8_t*)fields, sizeof(fields))); | |
| } | |
| char text[17]; | |
| snprintf(text, sizeof(text), "%016llx", (unsigned long long)key); | |
| return std::string(text); | |
| }(); | |
| return build; | |
| } | |
| // A loader fix changes no compiler key, so each build keeps its own marker. | |
| static std::string rejectedMarker(const std::string& artifactPath) | |
| { | |
| return artifactPath + "." + loaderBuild() + ".rejected"; | |
| } | |
| #ifdef _WIN32 | |
| // CreateProcess takes one command line; quote each argument so the child's | |
| // CRT splits it back into the same argv. | |
| static std::string quoteArg(const std::string& arg) | |
| { | |
| if (!arg.empty() && arg.find_first_of(" \t\"") == std::string::npos) | |
| { | |
| return arg; | |
| } | |
| std::string out = "\""; | |
| size_t slashes = 0; | |
| for (char c : arg) | |
| { | |
| if (c == '\\') | |
| { | |
| slashes++; | |
| continue; | |
| } | |
| out.append(c == '"' ? slashes * 2 + 1 : slashes, '\\'); | |
| slashes = 0; | |
| out += c; | |
| } | |
| out.append(slashes * 2, '\\'); | |
| return out + "\""; | |
| } | |
| static HANDLE inheritable(HANDLE handle) | |
| { | |
| HANDLE copy = nullptr; | |
| if (handle == nullptr || handle == INVALID_HANDLE_VALUE || | |
| !DuplicateHandle( | |
| GetCurrentProcess(), | |
| handle, | |
| GetCurrentProcess(), | |
| ©, | |
| 0, | |
| TRUE, | |
| DUPLICATE_SAME_ACCESS)) | |
| { | |
| return nullptr; | |
| } | |
| return copy; | |
| } | |
| // Starts args below interactive priority with no console window. stdout goes | |
| // to out, or nowhere; stderr is the host's unless quiet. Only these handles | |
| // are inherited, so a concurrent child never holds another's pipe open. | |
| static intptr_t startChild( | |
| const std::vector<std::string>& args, | |
| std::string* failure, | |
| HANDLE out = nullptr, | |
| bool quiet = false) | |
| { | |
| std::string line; | |
| for (const std::string& arg : args) | |
| { | |
| line += (line.empty() ? "" : " ") + quoteArg(arg); | |
| } | |
| SECURITY_ATTRIBUTES sa{sizeof(sa), nullptr, TRUE}; | |
| HANDLE nul = CreateFileA( | |
| "NUL", | |
| GENERIC_READ | GENERIC_WRITE, | |
| FILE_SHARE_READ | FILE_SHARE_WRITE, | |
| &sa, | |
| OPEN_EXISTING, | |
| 0, | |
| nullptr); | |
| HANDLE err = quiet ? nullptr : inheritable(GetStdHandle(STD_ERROR_HANDLE)); | |
| STARTUPINFOEXA si{}; | |
| si.StartupInfo.cb = sizeof(si); | |
| si.StartupInfo.dwFlags = STARTF_USESTDHANDLES; | |
| si.StartupInfo.hStdInput = nul; | |
| si.StartupInfo.hStdOutput = out != nullptr ? out : nul; | |
| si.StartupInfo.hStdError = err != nullptr ? err : nul; | |
| HANDLE handles[3] = { | |
| nul, | |
| si.StartupInfo.hStdOutput, | |
| si.StartupInfo.hStdError}; | |
| std::sort(std::begin(handles), std::end(handles)); | |
| DWORD handleCount = (DWORD)(std::unique( | |
| std::begin(handles), | |
| std::end(handles)) - | |
| handles); | |
| SIZE_T attrSize = 0; | |
| InitializeProcThreadAttributeList(nullptr, 1, 0, &attrSize); | |
| std::vector<uint8_t> attrs(attrSize); | |
| si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)attrs.data(); | |
| PROCESS_INFORMATION pi{}; | |
| bool listReady = InitializeProcThreadAttributeList( | |
| si.lpAttributeList, | |
| 1, | |
| 0, | |
| &attrSize); | |
| bool ok = listReady && | |
| UpdateProcThreadAttribute( | |
| si.lpAttributeList, | |
| 0, | |
| PROC_THREAD_ATTRIBUTE_HANDLE_LIST, | |
| handles, | |
| handleCount * sizeof(HANDLE), | |
| nullptr, | |
| nullptr) && | |
| CreateProcessA( | |
| nullptr, | |
| line.data(), | |
| nullptr, | |
| nullptr, | |
| TRUE, | |
| CREATE_NO_WINDOW | BELOW_NORMAL_PRIORITY_CLASS | | |
| EXTENDED_STARTUPINFO_PRESENT, | |
| nullptr, | |
| nullptr, | |
| &si.StartupInfo, | |
| &pi); | |
| DWORD error = ok ? 0 : GetLastError(); | |
| if (listReady) | |
| { | |
| DeleteProcThreadAttributeList(si.lpAttributeList); | |
| } | |
| CloseHandle(nul); | |
| if (err != nullptr) | |
| { | |
| CloseHandle(err); | |
| } | |
| if (!ok) | |
| { | |
| *failure = "could not run " + args[0] + ", windows error " + | |
| std::to_string(error); | |
| return -1; | |
| } | |
| CloseHandle(pi.hThread); | |
| return (intptr_t)pi.hProcess; | |
| } | |
| static void killChild(intptr_t child) { TerminateProcess((HANDLE)child, 1); } | |
| // The exit code, or -1 when the child did not exit on its own. | |
| static int waitChild(intptr_t child) | |
| { | |
| DWORD code = 1; | |
| if (WaitForSingleObject((HANDLE)child, INFINITE) != WAIT_OBJECT_0 || | |
| !GetExitCodeProcess((HANDLE)child, &code)) | |
| { | |
| return -1; | |
| } | |
| return (int)code; | |
| } | |
| static void releaseChild(intptr_t child) { CloseHandle((HANDLE)child); } | |
| // Runs wamrc with flag and --version: true when it exits cleanly, with the | |
| // first line of its output when asked. | |
| static bool probeWamrc( | |
| const std::string& wamrc, | |
| const char* flag, | |
| std::string* firstLine) | |
| { | |
| std::vector<std::string> args = {wamrc}; | |
| if (flag != nullptr) | |
| { | |
| args.push_back(flag); | |
| } | |
| args.push_back("--version"); | |
| SECURITY_ATTRIBUTES sa{sizeof(sa), nullptr, TRUE}; | |
| HANDLE readEnd = nullptr; | |
| HANDLE writeEnd = nullptr; | |
| if (!CreatePipe(&readEnd, &writeEnd, &sa, 0)) | |
| { | |
| return false; | |
| } | |
| SetHandleInformation(readEnd, HANDLE_FLAG_INHERIT, 0); | |
| std::string failure; | |
| intptr_t child = startChild(args, &failure, writeEnd, true); | |
| CloseHandle(writeEnd); | |
| std::string text; | |
| char buffer[256]; | |
| DWORD read = 0; | |
| while (ReadFile(readEnd, buffer, sizeof(buffer), &read, nullptr) && read > 0) | |
| { | |
| text.append(buffer, read); | |
| } | |
| CloseHandle(readEnd); | |
| if (child == -1) | |
| { | |
| return false; | |
| } | |
| int code = waitChild(child); | |
| releaseChild(child); | |
| if (firstLine != nullptr) | |
| { | |
| *firstLine = text.substr(0, text.find('\n')); | |
| } | |
| return code == 0; | |
| } | |
| #elif defined(RIVE_ANDROID) || defined(RIVE_NX) | |
| // No wamrc on device, and neither platform can spawn one. | |
| static intptr_t startChild( | |
| const std::vector<std::string>&, | |
| std::string* failure) | |
| { | |
| *failure = "wamrc cannot run on this platform"; | |
| return -1; | |
| } | |
| static void killChild(intptr_t) {} | |
| static int waitChild(intptr_t) { return -1; } | |
| static void releaseChild(intptr_t) {} | |
| #else | |
| // Starts args below interactive priority; a compiler's stdout is noise at | |
| // editor runtime, stderr stays for diagnosing failed compiles. | |
| static intptr_t startChild( | |
| const std::vector<std::string>& args, | |
| std::string* failure) | |
| { | |
| std::vector<char*> argv; | |
| for (const std::string& arg : args) | |
| { | |
| argv.push_back(const_cast<char*>(arg.c_str())); | |
| } | |
| argv.push_back(nullptr); | |
| pid_t pid = -1; | |
| posix_spawn_file_actions_t actions; | |
| posix_spawn_file_actions_init(&actions); | |
| posix_spawn_file_actions_addopen( | |
| &actions, | |
| STDOUT_FILENO, | |
| "/dev/null", | |
| O_WRONLY, | |
| 0); | |
| int rc = posix_spawn(&pid, argv[0], &actions, nullptr, argv.data(), environ); | |
| posix_spawn_file_actions_destroy(&actions); | |
| if (rc != 0) | |
| { | |
| *failure = "could not run " + args[0] + ", " + strerror(rc); | |
| return -1; | |
| } | |
| // The editor's frame loop must never contend with LLVM. | |
| setpriority(PRIO_PROCESS, pid, 10); | |
| return pid; | |
| } | |
| static void killChild(intptr_t child) { kill((pid_t)child, SIGKILL); } | |
| // The exit code, or -1 when the child did not exit on its own. | |
| static int waitChild(intptr_t child) | |
| { | |
| int status = 0; | |
| while (waitpid((pid_t)child, &status, 0) < 0 && errno == EINTR) | |
| { | |
| } | |
| return WIFEXITED(status) ? WEXITSTATUS(status) : -1; | |
| } | |
| static void releaseChild(intptr_t) {} | |
| #endif | |
| #if !defined(_WIN32) && !defined(RIVE_NX) | |
| // Installed apps put wamrc under paths with spaces. | |
| static std::string shellQuote(const std::string& text) | |
| { | |
| std::string quoted = "'"; | |
| for (char c : text) | |
| { | |
| quoted += c == '\'' ? std::string("'\\''") : std::string(1, c); | |
| } | |
| return quoted + "'"; | |
| } | |
| // Runs wamrc with flag and --version: true when it exits cleanly, with the | |
| // first line of its output when asked. | |
| static bool probeWamrc( | |
| const std::string& wamrc, | |
| const char* flag, | |
| std::string* firstLine) | |
| { | |
| std::string cmd = shellQuote(wamrc) + | |
| (flag != nullptr ? " " + std::string(flag) : "") + | |
| " --version" + | |
| (firstLine != nullptr ? " 2>/dev/null" | |
| : " >/dev/null 2>&1"); | |
| FILE* pipe = popen(cmd.c_str(), "r"); | |
| if (pipe == nullptr) | |
| { | |
| return false; | |
| } | |
| char line[128] = {0}; | |
| if (firstLine != nullptr && fgets(line, sizeof(line), pipe) != nullptr) | |
| { | |
| *firstLine = line; | |
| } | |
| return pclose(pipe) == 0; | |
| } | |
| #endif | |
| ModuleTierLadder& ModuleTierLadder::instance() | |
| { | |
| static ModuleTierLadder* ladder = new ModuleTierLadder(); | |
| return *ladder; | |
| } | |
| void ModuleTierLadder::configure( | |
| const std::string& wamrcPath, | |
| const std::string& cacheDir, | |
| bool compileOnBoot) | |
| { | |
| // Before a rebuild can replace the loader's binary on disk. | |
| loaderBuild(); | |
| std::unique_lock<std::mutex> lock(m_mutex); | |
| m_wamrcPath = wamrcPath; | |
| m_compileOnBoot = compileOnBoot; | |
| m_versionProbed = false; | |
| if (m_wamrcPath.empty()) | |
| { | |
| if (const char* env = getenv("RIVE_WAMRC")) | |
| { | |
| m_wamrcPath = env; | |
| } | |
| } | |
| m_cacheDir = cacheDir; | |
| m_failures.clear(); | |
| m_compiled.clear(); | |
| m_pruned = false; | |
| if (m_cacheDir.empty() && !m_wamrcPath.empty()) | |
| { | |
| const char* dirEnv = getenv("RIVE_AOT_CACHE_DIR"); | |
| #ifdef RIVE_NX | |
| // No temp_directory_path in this libc++, and the platform cannot map | |
| // executable pages anyway, so the ladder only runs when pointed at a | |
| // directory explicitly. | |
| m_cacheDir = dirEnv != nullptr ? dirEnv : ""; | |
| #else | |
| m_cacheDir = dirEnv != nullptr ? dirEnv : defaultCacheDir(); | |
| #endif | |
| } | |
| } | |
| void ModuleTierLadder::unconfigure() | |
| { | |
| std::unique_lock<std::mutex> lock(m_mutex); | |
| m_wamrcPath.clear(); | |
| m_cacheDir.clear(); | |
| m_failures.clear(); | |
| m_compiled.clear(); | |
| m_versionProbed = false; | |
| m_compileOnBoot = false; | |
| } | |
| bool ModuleTierLadder::enabled() | |
| { | |
| std::unique_lock<std::mutex> lock(m_mutex); | |
| return enabledLocked(); | |
| } | |
| bool ModuleTierLadder::enabledLocked() | |
| { | |
| if (m_wamrcPath.empty()) | |
| { | |
| m_disabledReason = | |
| "no wamrc, set RIVE_WAMRC or pass --aot with our wamrc on PATH"; | |
| return false; | |
| } | |
| if (m_cacheDir.empty()) | |
| { | |
| m_disabledReason = "no aot cache, set RIVE_AOT_CACHE_DIR"; | |
| return false; | |
| } | |
| if (!trustedCacheDir(m_cacheDir)) | |
| { | |
| m_disabledReason = "the aot cache " + m_cacheDir + | |
| " is not private to this user, run chmod go-w on it"; | |
| return false; | |
| } | |
| wamrcVersion(); | |
| m_disabledReason = m_wamrcUsable ? "" : m_wamrcPath + " " + m_wamrcProblem; | |
| return m_wamrcUsable; | |
| } | |
| std::string ModuleTierLadder::disabledReason() | |
| { | |
| if (enabled()) | |
| { | |
| return std::string(); | |
| } | |
| std::unique_lock<std::mutex> lock(m_mutex); | |
| return m_disabledReason; | |
| } | |
| std::string ModuleTierLadder::untrustedCacheDir() | |
| { | |
| std::unique_lock<std::mutex> lock(m_mutex); | |
| return m_cacheDir.empty() || trustedCacheDir(m_cacheDir) ? std::string() | |
| : m_cacheDir; | |
| } | |
| std::string ModuleTierLadder::compilerPath() | |
| { | |
| std::unique_lock<std::mutex> lock(m_mutex); | |
| return m_wamrcPath; | |
| } | |
| const std::string& ModuleTierLadder::wamrcVersion() | |
| { | |
| // Callers hold m_mutex. The version keys the cache so a compiler swap | |
| // reads as misses, never as stale native code. | |
| if (!m_versionProbed) | |
| { | |
| m_versionProbed = true; | |
| m_wamrcVersion = "unknown"; | |
| #ifndef RIVE_NX | |
| std::string v; | |
| probeWamrc(m_wamrcPath, nullptr, &v); | |
| while (!v.empty() && | |
| (v.back() == '\n' || v.back() == '\r' || v.back() == ' ')) | |
| { | |
| v.pop_back(); | |
| } | |
| for (char& c : v) | |
| { | |
| if (c == ' ' || c == '/') | |
| { | |
| c = '-'; | |
| } | |
| } | |
| if (!v.empty()) | |
| { | |
| m_wamrcVersion = v; | |
| } | |
| // Every build of our patched wamrc prints the same version. | |
| uint64_t binaryKey = 0; | |
| fileContentKey(m_wamrcPath, binaryKey); | |
| char binary[20]; | |
| snprintf(binary, sizeof(binary), "-%016llx", (unsigned long long)binaryKey); | |
| m_wamrcVersion += binary; | |
| auto accepts = [&](const char* flag) { | |
| return probeWamrc(m_wamrcPath, flag, nullptr); | |
| }; | |
| // Only our patched wamrc takes --rive-interrupt, and only a current one | |
| // the guard flag; say so once rather than fail each compile quietly. | |
| m_wamrcProblem.clear(); | |
| if (!accepts("--rive-interrupt")) | |
| { | |
| m_wamrcProblem = "is not our patched wamrc; build wamrc from our WAMR " | |
| "patches (runtime/scripting/wamr_patches)"; | |
| } | |
| else if (!accepts(wamrcHwGuardFlag().c_str())) | |
| { | |
| m_wamrcProblem = "predates our current WAMR patches; rebuild wamrc " | |
| "from the current patches " | |
| "(runtime/scripting/wamr_patches)"; | |
| } | |
| m_wamrcUsable = m_wamrcProblem.empty(); | |
| m_wamrcTunes = accepts("--tune-cpu=generic"); | |
| if (!m_wamrcUsable) | |
| { | |
| fprintf( | |
| stderr, | |
| "wasm aot: %s %s. AOT is off.\n", | |
| m_wamrcPath.c_str(), | |
| m_wamrcProblem.c_str()); | |
| } | |
| #endif | |
| } | |
| return m_wamrcVersion; | |
| } | |
| // wamrc flags naming a device target, null for one we cannot compile for. | |
| static const char* const* crossTargetArgs(const std::string& target) | |
| { | |
| // Android reserves x18 for its shadow call stack. | |
| static const char* const androidAarch64[] = { | |
| "--target=aarch64v8", | |
| "--target-abi=gnu", | |
| "--cpu=generic", | |
| "--cpu-features=+reserve-x18", | |
| nullptr}; | |
| // wamrc compiles SIMD only for SSE4.1, which the NDK x86_64 ABI | |
| // guarantees. | |
| static const char* const androidX64[] = | |
| {"--target=x86_64", "--target-abi=gnu", "--cpu=x86-64-v2", nullptr}; | |
| if (target == kAotTargetAndroidAarch64) | |
| { | |
| return androidAarch64; | |
| } | |
| if (target == kAotTargetAndroidX64) | |
| { | |
| return androidX64; | |
| } | |
| return nullptr; | |
| } | |
| static bool asksHostBudget(const std::string& wasmPath) | |
| { | |
| std::ifstream in(wasmPath, std::ios::binary); | |
| std::vector<uint8_t> bytes( | |
| (std::istreambuf_iterator<char>(in)), | |
| std::istreambuf_iterator<char>()); | |
| return exportsFunction(bytes, "__riveHostBudget"); | |
| } | |
| std::string ModuleTierLadder::keyedCacheDir() | |
| { | |
| // The revision folds our wamrc flag choices into the cache key; bump | |
| // it whenever species flags change or stale artifacts get served on a | |
| // cache hit. | |
| const std::string& version = wamrcVersion(); | |
| return m_cacheDir + "/" + version + "-r18-g" + | |
| std::to_string(WASM_RIVE_HW_GUARD_SIZE) + (m_wamrcTunes ? "t" : ""); | |
| } | |
| // Device artifacts sit apart from this machine's, which share their names. | |
| std::string ModuleTierLadder::artifactDir(const std::string& target) | |
| { | |
| std::string dir = keyedCacheDir(); | |
| if (!target.empty()) | |
| { | |
| return dir + "/" + target; | |
| } | |
| // A host sharing the cache finds only code its cpu runs. | |
| return hostX86_64Level() >= 3 ? dir + "/x86-64-v3" : dir; | |
| } | |
| void ModuleTierLadder::createArtifactDir(const std::string& target) | |
| { | |
| makeDir(m_cacheDir); | |
| makeDir(keyedCacheDir()); | |
| makeDir(artifactDir(target)); | |
| } | |
| std::string ModuleTierLadder::artifactName( | |
| uint64_t moduleKey, | |
| TierSpecies species) | |
| { | |
| if (species == TierSpecies::o0) | |
| { | |
| return aotArtifactName(moduleKey, ".o0"); | |
| } | |
| if (species == TierSpecies::hw) | |
| { | |
| return aotArtifactName(moduleKey, ".hw"); | |
| } | |
| return aotArtifactName(moduleKey); | |
| } | |
| std::string ModuleTierLadder::artifactPath( | |
| uint64_t moduleKey, | |
| TierSpecies species, | |
| const std::string& target) | |
| { | |
| std::unique_lock<std::mutex> lock(m_mutex); | |
| // Targets come from link peers, so only known ones reach a path. | |
| if (m_wamrcPath.empty() || m_cacheDir.empty() || | |
| !trustedCacheDir(m_cacheDir) || | |
| (!target.empty() && crossTargetArgs(target) == nullptr)) | |
| { | |
| return std::string(); | |
| } | |
| std::string path = artifactDir(target) + "/" + | |
| artifactName(moduleKey, species); | |
| return fileExists(path) ? path : std::string(); | |
| } | |
| #ifndef RIVE_NX | |
| // Another process may still be using an older compiler's key dir this long | |
| // after its last compile. | |
| constexpr auto kStaleKeyDirAge = std::chrono::hours(24 * 7); | |
| constexpr uintmax_t kKeyDirBytes = 512ull << 20; | |
| // The names every keyedCacheDir has had: a wamrc version line, then a | |
| // revision. | |
| static bool isKeyDirName(const std::string& name) | |
| { | |
| size_t revision = name.rfind("-r"); | |
| if ((name.rfind("wamrc-", 0) != 0 && name.rfind("unknown-", 0) != 0) || | |
| revision == std::string::npos) | |
| { | |
| return false; | |
| } | |
| std::string digits = name.substr(revision + 2); | |
| if (!digits.empty() && digits.back() == 't') | |
| { | |
| digits.pop_back(); | |
| } | |
| return !digits.empty() && | |
| digits.find_first_not_of("0123456789") == std::string::npos; | |
| } | |
| // Every module and every wamrc update leave artifacts behind, so stale key | |
| // dirs go, then this one's oldest compiles until it fits. | |
| static void pruneCache( | |
| const std::string& cacheDir, | |
| const std::string& keyedDir, | |
| bool staleKeyDirs) | |
| { | |
| namespace fs = std::filesystem; | |
| std::error_code ec; | |
| auto staleBefore = fs::file_time_type::clock::now() - kStaleKeyDirAge; | |
| if (staleKeyDirs) | |
| { | |
| std::string current = fs::path(keyedDir).filename().string(); | |
| std::vector<fs::path> stale; | |
| for (fs::directory_iterator it(cacheDir, ec), end; !ec && it != end; | |
| it.increment(ec)) | |
| { | |
| std::string name = it->path().filename().string(); | |
| std::error_code statError; | |
| if (name != current && isKeyDirName(name) && | |
| it->is_directory(statError) && | |
| it->last_write_time(statError) < staleBefore && !statError) | |
| { | |
| stale.push_back(it->path()); | |
| } | |
| } | |
| for (const fs::path& dir : stale) | |
| { | |
| fs::remove_all(dir, ec); | |
| } | |
| } | |
| struct Artifact | |
| { | |
| fs::file_time_type written; | |
| uintmax_t size; | |
| fs::path path; | |
| }; | |
| std::vector<Artifact> artifacts; | |
| std::vector<fs::path> staleMarkers; | |
| uintmax_t total = 0; | |
| ec.clear(); | |
| for (fs::recursive_directory_iterator it(keyedDir, ec), end; !ec && it != end; | |
| it.increment(ec)) | |
| { | |
| std::string extension = it->path().extension().string(); | |
| std::error_code statError; | |
| // Each build leaves its own markers, so old ones expire like key dirs. | |
| if (extension == ".rejected" && | |
| it->last_write_time(statError) < staleBefore && !statError) | |
| { | |
| staleMarkers.push_back(it->path()); | |
| } | |
| else if ( | |
| (extension == ".aot" || extension == ".wasm") && | |
| it->is_regular_file(statError)) | |
| { | |
| Artifact artifact{ | |
| it->last_write_time(statError), | |
| it->file_size(statError), | |
| it->path()}; | |
| if (!statError) | |
| { | |
| total += artifact.size; | |
| artifacts.push_back(std::move(artifact)); | |
| } | |
| } | |
| } | |
| for (const fs::path& marker : staleMarkers) | |
| { | |
| fs::remove(marker, ec); | |
| } | |
| if (total <= kKeyDirBytes) | |
| { | |
| return; | |
| } | |
| std::sort( | |
| artifacts.begin(), | |
| artifacts.end(), | |
| [](const Artifact& a, const Artifact& b) { | |
| return a.written < b.written; | |
| }); | |
| for (const Artifact& artifact : artifacts) | |
| { | |
| if (total <= kKeyDirBytes) | |
| { | |
| break; | |
| } | |
| if (fs::remove(artifact.path, ec)) | |
| { | |
| total -= artifact.size; | |
| } | |
| } | |
| } | |
| #endif | |
| // The cache is shared across processes and a name must never be visible | |
| // half-written: write a process-unique temp, then atomically rename. | |
| static bool writeFileAtomic(const std::string& path, Span<const uint8_t> bytes) | |
| { | |
| std::string tmpPath = path + "." + std::to_string(processId()) + ".tmp"; | |
| FILE* f = fopen(tmpPath.c_str(), "wb"); | |
| if (f == nullptr) | |
| { | |
| return false; | |
| } | |
| size_t written = fwrite(bytes.data(), 1, bytes.size(), f); | |
| if (fclose(f) != 0 || written != bytes.size()) | |
| { | |
| std::remove(tmpPath.c_str()); | |
| return false; | |
| } | |
| return publishFile(tmpPath, path); | |
| } | |
| TierCompile ModuleTierLadder::compileSync( | |
| uint64_t moduleKey, | |
| Span<const uint8_t> moduleBytes, | |
| TierSpecies species, | |
| const std::string& target, | |
| const std::atomic<bool>* cancel) | |
| { | |
| TierCompile result; | |
| Job job{moduleKey, species}; | |
| job.target = target; | |
| job.cancel = cancel; | |
| std::string cacheDir; | |
| std::string keyedDir; | |
| bool staleKeyDirs = false; | |
| { | |
| std::unique_lock<std::mutex> lock(m_mutex); | |
| if (lookupLocked(job, result)) | |
| { | |
| return result; | |
| } | |
| createArtifactDir(target); | |
| // Another user may have made the dir first. | |
| if (!enabledLocked()) | |
| { | |
| result.reason = m_disabledReason; | |
| return result; | |
| } | |
| cacheDir = m_cacheDir; | |
| keyedDir = keyedCacheDir(); | |
| job.wasmPath = keyedDir + "/" + wasmModuleName(moduleKey); | |
| if (!fileExists(job.wasmPath) && | |
| !writeFileAtomic(job.wasmPath, moduleBytes)) | |
| { | |
| result.reason = "could not write the module into the aot cache " + | |
| m_cacheDir; | |
| m_failures[{moduleKey, species, target}] = result.reason; | |
| return result; | |
| } | |
| if (cancel != nullptr) | |
| { | |
| m_cancelableJobs.push_back(&job); | |
| } | |
| } | |
| auto compileStart = std::chrono::steady_clock::now(); | |
| result.reason = runWamrc(job); | |
| { | |
| std::unique_lock<std::mutex> lock(m_mutex); | |
| if (cancel != nullptr) | |
| { | |
| m_cancelableJobs.erase( | |
| std::find(m_cancelableJobs.begin(), m_cancelableJobs.end(), &job)); | |
| } | |
| if (!result.reason.empty() && !job.cancelled) | |
| { | |
| m_failures[{moduleKey, species, target}] = result.reason; | |
| } | |
| if (result.reason.empty() && target.empty()) | |
| { | |
| m_compiled.insert({moduleKey, species}); | |
| } | |
| staleKeyDirs = result.reason.empty() && !m_pruned; | |
| m_pruned = m_pruned || staleKeyDirs; | |
| } | |
| if (result.reason.empty()) | |
| { | |
| result.path = job.finalPath; | |
| result.compiled = true; | |
| if (target.empty()) | |
| { | |
| auto compileMs = std::chrono::duration_cast<std::chrono::milliseconds>( | |
| std::chrono::steady_clock::now() - compileStart) | |
| .count(); | |
| fprintf( | |
| stderr, | |
| "wasm aot: compiled %s in %lld ms\n", | |
| result.path.c_str(), | |
| (long long)compileMs); | |
| } | |
| #ifndef RIVE_NX | |
| pruneCache(cacheDir, keyedDir, staleKeyDirs); | |
| #endif | |
| } | |
| return result; | |
| } | |
| std::vector<TierCompile> ModuleTierLadder::compileAll( | |
| const std::vector<Span<const uint8_t>>& modules, | |
| TierSpecies species, | |
| const std::atomic<bool>* cancel) | |
| { | |
| std::vector<uint64_t> keys; | |
| for (Span<const uint8_t> module : modules) | |
| { | |
| keys.push_back(wasmModuleKey(module)); | |
| } | |
| std::vector<TierCompile> results(modules.size()); | |
| std::vector<size_t> misses; | |
| { | |
| std::unique_lock<std::mutex> lock(m_mutex); | |
| for (size_t i = 0; i < modules.size(); i++) | |
| { | |
| Job job{keys[i], species}; | |
| if (!lookupLocked(job, results[i])) | |
| { | |
| misses.push_back(i); | |
| } | |
| } | |
| } | |
| auto compile = [&](size_t i) { | |
| if (cancel != nullptr && cancel->load()) | |
| { | |
| results[i].reason = kCancelledReason; | |
| return; | |
| } | |
| results[i] = compileSync( | |
| keys[i], | |
| modules[i], | |
| species, | |
| std::string(), | |
| cancel); | |
| }; | |
| std::vector<std::thread> threads; | |
| for (size_t i = 1; i < misses.size(); i++) | |
| { | |
| threads.emplace_back(compile, misses[i]); | |
| } | |
| if (!misses.empty()) | |
| { | |
| compile(misses.front()); | |
| } | |
| for (std::thread& thread : threads) | |
| { | |
| thread.join(); | |
| } | |
| return results; | |
| } | |
| std::vector<TierCompile> ModuleTierLadder::compileWithLanes( | |
| Span<const uint8_t> moduleBytes, | |
| TierSpecies species, | |
| const std::atomic<bool>* cancel) | |
| { | |
| std::vector<std::vector<uint8_t>> lanes = laneModules(moduleBytes); | |
| std::vector<Span<const uint8_t>> modules = {moduleBytes}; | |
| for (const std::vector<uint8_t>& lane : lanes) | |
| { | |
| modules.emplace_back(lane.data(), lane.size()); | |
| } | |
| return compileAll(modules, species, cancel); | |
| } | |
| bool ModuleTierLadder::lookupLocked(Job& job, TierCompile& result) | |
| { | |
| if (!enabledLocked()) | |
| { | |
| result.reason = m_disabledReason; | |
| return true; | |
| } | |
| if (!job.target.empty() && crossTargetArgs(job.target) == nullptr) | |
| { | |
| result.reason = "wamrc cannot compile for " + job.target; | |
| return true; | |
| } | |
| auto failure = m_failures.find({job.moduleKey, job.species, job.target}); | |
| if (failure != m_failures.end()) | |
| { | |
| result.reason = failure->second; | |
| return true; | |
| } | |
| job.finalPath = artifactDir(job.target) + "/" + | |
| artifactName(job.moduleKey, job.species); | |
| if (fileExists(job.finalPath)) | |
| { | |
| result.path = job.finalPath; | |
| return true; | |
| } | |
| std::ifstream rejected(rejectedMarker(job.finalPath)); | |
| if (rejected && std::getline(rejected, result.reason)) | |
| { | |
| m_failures[{job.moduleKey, job.species, job.target}] = result.reason; | |
| return true; | |
| } | |
| return false; | |
| } | |
| TierCompile ModuleTierLadder::bootCompile( | |
| uint64_t moduleKey, | |
| Span<const uint8_t> moduleBytes, | |
| TierSpecies species) | |
| { | |
| { | |
| std::unique_lock<std::mutex> lock(m_mutex); | |
| if (!m_compileOnBoot) | |
| { | |
| TierCompile result; | |
| Job job{moduleKey, species}; | |
| if (!lookupLocked(job, result)) | |
| { | |
| result.reason = "no aot artifact cached for this module yet"; | |
| } | |
| return result; | |
| } | |
| } | |
| // The boot loads its lanes next, and compiles of one module never merge, | |
| // so they all finish before it returns. | |
| return compileWithLanes(moduleBytes, species).front(); | |
| } | |
| bool ModuleTierLadder::failed( | |
| uint64_t moduleKey, | |
| TierSpecies species, | |
| const std::string& target) | |
| { | |
| std::unique_lock<std::mutex> lock(m_mutex); | |
| return m_failures.count({moduleKey, species, target}) != 0; | |
| } | |
| void ModuleTierLadder::rejectArtifact( | |
| uint64_t moduleKey, | |
| TierSpecies species, | |
| const std::string& reason) | |
| { | |
| std::unique_lock<std::mutex> lock(m_mutex); | |
| m_failures[{moduleKey, species, std::string()}] = reason; | |
| if (m_cacheDir.empty()) | |
| { | |
| return; | |
| } | |
| std::string path = artifactDir(std::string()) + "/" + | |
| artifactName(moduleKey, species); | |
| std::remove(path.c_str()); | |
| // A recompile would make the same artifact again, so later sessions skip | |
| // it. One an older session left, maybe torn, compiles afresh. | |
| if (m_compiled.count({moduleKey, species}) != 0) | |
| { | |
| writeFileAtomic( | |
| rejectedMarker(path), | |
| Span<const uint8_t>((const uint8_t*)reason.data(), reason.size())); | |
| } | |
| } | |
| void ModuleTierLadder::cancelCompiles() | |
| { | |
| std::unique_lock<std::mutex> lock(m_mutex); | |
| for (Job* job : m_cancelableJobs) | |
| { | |
| if (job->cancel == nullptr || !job->cancel->load()) | |
| { | |
| continue; | |
| } | |
| job->cancelled = true; | |
| if (job->child != -1) | |
| { | |
| killChild(job->child); | |
| } | |
| } | |
| } | |
| std::string ModuleTierLadder::runWamrc(Job& job) | |
| { | |
| // Callers dropped m_mutex; only job fields and immutable config are | |
| // touched off-lock, except child which cancelCompiles reads under | |
| // lock. | |
| const std::string& finalPath = job.finalPath; | |
| std::string wamrc; | |
| std::string cacheDir; | |
| [[maybe_unused]] bool tunes = false; | |
| { | |
| std::unique_lock<std::mutex> lock(m_mutex); | |
| wamrc = m_wamrcPath; | |
| cacheDir = m_cacheDir; | |
| tunes = m_wamrcTunes; | |
| } | |
| // Unique temp per compile: the same artifact can be raced by two boots | |
| // on different threads, or by two processes sharing the cache. | |
| static std::atomic<uint64_t> tmpSerial{0}; | |
| std::string tmpPath = finalPath + "." + std::to_string(processId()) + "-" + | |
| std::to_string(++tmpSerial) + ".tmp"; | |
| std::vector<std::string> args = {wamrc}; | |
| if (job.species == TierSpecies::hw) | |
| { | |
| // Guard-page bounds; native stack checks stay sw (the runtime | |
| // builds with WASM_DISABLE_STACK_HW_BOUND_CHECK). | |
| args.push_back("--bounds-checks=0"); | |
| args.push_back("--stack-bounds-checks=1"); | |
| args.push_back(wamrcHwGuardFlag()); | |
| } | |
| else | |
| { | |
| // sw bounds run on every build; wamrc's default hw-bounds output | |
| // segfaults on runtimes without the guard-page trap handler. | |
| args.push_back("--bounds-checks=1"); | |
| } | |
| if (job.species == TierSpecies::o0) | |
| { | |
| args.push_back("--opt-level=0"); | |
| } | |
| else | |
| { | |
| args.push_back("--opt-level=3"); | |
| } | |
| if (!job.target.empty()) | |
| { | |
| const char* const* targetArgs = crossTargetArgs(job.target); | |
| if (targetArgs == nullptr) | |
| { | |
| return "wamrc cannot compile for " + job.target; | |
| } | |
| for (; *targetArgs != nullptr; targetArgs++) | |
| { | |
| args.push_back(*targetArgs); | |
| } | |
| } | |
| #if (defined(__APPLE__) || defined(_WIN32)) && \ | |
| (defined(__aarch64__) || defined(_M_ARM64)) | |
| else | |
| { | |
| // The platform clobbers x18; reserve it here so safety does not | |
| // depend on the wamrc we were handed. Features need an explicit cpu. | |
| args.push_back("--cpu=generic"); | |
| #if defined(__APPLE__) | |
| // Every Apple silicon Mac has single instruction atomics, which make | |
| // parallel calls' barriers cheaper than load and store exclusive loops. | |
| args.push_back("--cpu-features=+reserve-x18,+lse"); | |
| #else | |
| args.push_back("--cpu-features=+reserve-x18"); | |
| #endif | |
| #if defined(__APPLE__) | |
| // Generic scheduling turns float compare chains into mispredicted | |
| // branches (up to 1.5x slower); tuning leaves the ISA baseline alone. | |
| if (tunes) | |
| { | |
| args.push_back("--tune-cpu=apple-m1"); | |
| } | |
| #endif | |
| } | |
| #elif defined(__x86_64__) || defined(_M_X64) | |
| else | |
| { | |
| // Left alone wamrc targets this machine's exact cpu, which the cache | |
| // does not key. Without FMA, relaxed madd calls fmaf. | |
| args.push_back("--target=x86_64"); | |
| args.push_back( | |
| hostX86_64Level() >= 3 ? "--cpu=x86-64-v3" : "--cpu=x86-64-v2"); | |
| #if defined(__APPLE__) | |
| // Code maps above 4GB here, out of reach of 32 bit relocations. | |
| args.push_back("--size-level=0"); | |
| #elif defined(_WIN32) | |
| // wamrc's own default for this host, which naming a target drops. | |
| args.push_back("--size-level=1"); | |
| #endif | |
| } | |
| #endif | |
| // Loop back-edges and calls check the watchdog only in modules that ask | |
| // for it; the module bytes, and so its cache key, decide. | |
| if (asksHostBudget(job.wasmPath)) | |
| { | |
| args.push_back("--rive-interrupt"); | |
| } | |
| args.push_back("-o"); | |
| args.push_back(tmpPath); | |
| args.push_back(job.wasmPath); | |
| std::string failure; | |
| intptr_t child = startChild(args, &failure); | |
| if (child == -1) | |
| { | |
| fprintf(stderr, "[wasm] %s\n", failure.c_str()); | |
| return failure; | |
| } | |
| { | |
| std::unique_lock<std::mutex> lock(m_mutex); | |
| job.child = child; | |
| // A cancel that landed before the spawn had nothing to kill. | |
| if (job.cancel != nullptr && job.cancel->load()) | |
| { | |
| job.cancelled = true; | |
| } | |
| if (job.cancelled) | |
| { | |
| killChild(child); | |
| } | |
| } | |
| int exitCode = waitChild(child); | |
| { | |
| std::unique_lock<std::mutex> lock(m_mutex); | |
| job.child = -1; | |
| } | |
| releaseChild(child); | |
| if (job.cancelled || exitCode != 0) | |
| { | |
| std::remove(tmpPath.c_str()); | |
| if (job.cancelled) | |
| { | |
| return kCancelledReason; | |
| } | |
| // wamrc prints usage errors to stdout, which is discarded; an | |
| // unlogged exit here is otherwise a silently dead lane. | |
| fprintf( | |
| stderr, | |
| "[wasm] wamrc failed (exit %d) on %s\n", | |
| exitCode, | |
| job.wasmPath.c_str()); | |
| return "wamrc could not compile the module"; | |
| } | |
| // Atomic arrival: a partial artifact can never carry the final name. | |
| if (!publishFile(tmpPath, finalPath)) | |
| { | |
| return "could not write the artifact into the aot cache " + cacheDir; | |
| } | |
| return std::string(); | |
| } | |
| } // namespace rive | |
| #endif // RIVE_WASM_TIER_LADDER | |
| #endif // WITH_RIVE_SCRIPTING_WASM |