Jonas Devlieghere 8d59cca1ab
[lldb] Add WebAssembly platform (#171507)
This PR adds a platform for WebAssembly. Heavily inspired by Pavel's
QemuUser, the platform lets you configure a WebAssembly runtime to run a
Wasm binary.

For example, the following configuration can be used to launch binaries
under the WebAssembly Micro Runtime (WARM):

```
settings set -- platform.plugin.wasm.runtime-args --heap-size=1048576
settings set -- platform.plugin.wasm.port-arg -g=127.0.0.1:
settings set -- platform.plugin.wasm.runtime-path /path/to/iwasm-2.4.0
```

With the settings above, you can now launch a binary directly under
WAMR:

```
❯ lldb simple.wasm
(lldb) target create "/Users/jonas/wasm-micro-runtime/product-mini/platforms/darwin/build/simple.wasm"
Current executable set to '/Users/jonas/wasm-micro-runtime/product-mini/platforms/darwin/build/simple.wasm' (wasm32).
(lldb) b main
Breakpoint 1: 2 locations.
(lldb) r
Process 1 launched: '/Users/jonas/wasm-micro-runtime/product-mini/platforms/darwin/build/simple.wasm' (wasm32)
2 locations added to breakpoint 1
[22:28:05:124 - 16FE27000]: control thread of debug object 0x1005e9020 start

[22:28:05:124 - 16FE27000]: Debug server listening on 127.0.0.1:49170

the module name is /Users/jonas/wasm-micro-runtime/product-mini/platforms/darwin/build/simple.wasm
Process 1 stopped
* thread #1, name = 'nobody', stop reason = breakpoint 1.3
    frame #0: 0x40000000000001d3 simple.wasm`main at simple.c:8:7
   5    }
   6
   7    int main() {
-> 8      int i = 1;
   9      int j = 2;
   10     return add(i, j);
   11   }
(lldb)
```
2025-12-11 11:12:26 -08:00

214 lines
6.7 KiB
C++

//===----------------------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "Plugins/Platform/WebAssembly/PlatformWasm.h"
#include "Plugins/Process/wasm/ProcessWasm.h"
#include "lldb/Core/PluginManager.h"
#include "lldb/Host/FileSystem.h"
#include "lldb/Host/ProcessLaunchInfo.h"
#include "lldb/Host/common/TCPSocket.h"
#include "lldb/Interpreter/OptionValueProperties.h"
#include "lldb/Target/Process.h"
#include "lldb/Target/Target.h"
#include "lldb/Utility/LLDBLog.h"
#include "lldb/Utility/Listener.h"
#include "lldb/Utility/Log.h"
#include "llvm/ADT/StringExtras.h"
using namespace lldb;
using namespace lldb_private;
LLDB_PLUGIN_DEFINE(PlatformWasm)
namespace {
#define LLDB_PROPERTIES_platformwasm
#include "PlatformWasmProperties.inc"
enum {
#define LLDB_PROPERTIES_platformwasm
#include "PlatformWasmPropertiesEnum.inc"
};
class PluginProperties : public Properties {
public:
PluginProperties() {
m_collection_sp = std::make_shared<OptionValueProperties>(
PlatformWasm::GetPluginNameStatic());
m_collection_sp->Initialize(g_platformwasm_properties);
}
FileSpec GetRuntimePath() const {
return GetPropertyAtIndexAs<FileSpec>(ePropertyRuntimePath, {});
}
Args GetRuntimeArgs() const {
Args result;
m_collection_sp->GetPropertyAtIndexAsArgs(ePropertyRuntimeArgs, result);
return result;
}
llvm::StringRef GetPortArg() const {
return GetPropertyAtIndexAs<llvm::StringRef>(ePropertyPortArg, {});
}
};
} // namespace
static PluginProperties &GetGlobalProperties() {
static PluginProperties g_settings;
return g_settings;
}
llvm::StringRef PlatformWasm::GetPluginDescriptionStatic() {
return "Platform for debugging Wasm";
}
void PlatformWasm::Initialize() {
PluginManager::RegisterPlugin(
GetPluginNameStatic(), GetPluginDescriptionStatic(),
PlatformWasm::CreateInstance, PlatformWasm::DebuggerInitialize);
}
void PlatformWasm::Terminate() {
PluginManager::UnregisterPlugin(PlatformWasm::CreateInstance);
}
void PlatformWasm::DebuggerInitialize(Debugger &debugger) {
if (!PluginManager::GetSettingForPlatformPlugin(debugger,
GetPluginNameStatic())) {
PluginManager::CreateSettingForPlatformPlugin(
debugger, GetGlobalProperties().GetValueProperties(),
"Properties for the wasm platform plugin.",
/*is_global_property=*/true);
}
}
PlatformSP PlatformWasm::CreateInstance(bool force, const ArchSpec *arch) {
Log *log = GetLog(LLDBLog::Platform);
LLDB_LOG(log, "force = {0}, arch = ({1}, {2})", force,
arch ? arch->GetArchitectureName() : "<null>",
arch ? arch->GetTriple().getTriple() : "<null>");
bool create = force;
if (!create && arch && arch->IsValid()) {
const llvm::Triple &triple = arch->GetTriple();
switch (triple.getArch()) {
case llvm::Triple::wasm32:
case llvm::Triple::wasm64:
create = true;
break;
default:
break;
}
}
LLDB_LOG(log, "create = {0}", create);
return create ? PlatformSP(new PlatformWasm()) : PlatformSP();
}
std::vector<ArchSpec>
PlatformWasm::GetSupportedArchitectures(const ArchSpec &process_host_arch) {
return {ArchSpec("wasm32-unknown-unknown-wasm"),
ArchSpec("wasm64-unknown-unknown-wasm")};
}
static auto get_arg_range(const Args &args) {
return llvm::make_range(args.GetArgumentArrayRef().begin(),
args.GetArgumentArrayRef().end());
}
lldb::ProcessSP PlatformWasm::DebugProcess(ProcessLaunchInfo &launch_info,
Debugger &debugger, Target &target,
Status &error) {
Log *log = GetLog(LLDBLog::Platform);
const PluginProperties &properties = GetGlobalProperties();
FileSpec runtime = properties.GetRuntimePath();
FileSystem::Instance().ResolveExecutableLocation(runtime);
if (!FileSystem::Instance().Exists(runtime)) {
error = Status::FromErrorStringWithFormatv(
"WebAssembly runtime does not exist: {0}", runtime.GetPath());
return nullptr;
}
uint16_t port = 0;
{
// Get the next available port by binding a socket to port 0.
TCPSocket listen_socket(true);
error = listen_socket.Listen("localhost:0", /*backlog=*/5);
if (error.Fail())
return nullptr;
port = listen_socket.GetLocalPortNumber();
}
if (error.Fail())
return nullptr;
Args args({runtime.GetPath(),
llvm::formatv("{0}{1}", properties.GetPortArg(), port).str()});
args.AppendArguments(properties.GetRuntimeArgs());
args.AppendArguments(launch_info.GetArguments());
launch_info.SetArguments(args, true);
launch_info.SetLaunchInSeparateProcessGroup(true);
launch_info.GetFlags().Clear(eLaunchFlagDebug);
auto exit_code = std::make_shared<std::optional<int>>();
launch_info.SetMonitorProcessCallback(
[=](lldb::pid_t pid, int signal, int status) {
LLDB_LOG(
log,
"WebAssembly runtime exited: pid = {0}, signal = {1}, status = {2}",
pid, signal, status);
exit_code->emplace(status);
});
// This is automatically done for host platform in
// Target::FinalizeFileActions, but we're not a host platform.
llvm::Error Err = launch_info.SetUpPtyRedirection();
LLDB_LOG_ERROR(log, std::move(Err), "SetUpPtyRedirection failed: {0}");
LLDB_LOG(log, "{0}", get_arg_range(launch_info.GetArguments()));
error = Host::LaunchProcess(launch_info);
if (error.Fail())
return nullptr;
ProcessSP process_sp = target.CreateProcess(
launch_info.GetListener(), wasm::ProcessWasm::GetPluginNameStatic(),
nullptr, true);
if (!process_sp) {
error = Status::FromErrorString("failed to create WebAssembly process");
return nullptr;
}
process_sp->HijackProcessEvents(launch_info.GetHijackListener());
error = process_sp->ConnectRemote(
llvm::formatv("connect://localhost:{0}", port).str());
if (error.Fail()) {
// If we know the runtime has exited, that's a better error message than
// failing to connect.
if (*exit_code)
error = Status::FromError(llvm::joinErrors(
llvm::createStringError(llvm::formatv(
"WebAssembly runtime exited with exit code {0}", **exit_code)),
error.takeError()));
return nullptr;
}
if (launch_info.GetPTY().GetPrimaryFileDescriptor() !=
PseudoTerminal::invalid_fd)
process_sp->SetSTDIOFileDescriptor(
launch_info.GetPTY().ReleasePrimaryFileDescriptor());
return process_sp;
}