Files
Jonathan Hui 27321a2110 [nexus] add WebAssembly support using Emscripten (#12904)
This commit adds support for building the Nexus simulator for
WebAssembly (WASM) using the Emscripten toolchain. This enables the
simulator to run in a web browser environment with a JavaScript-based
control interface and visualization.

Key implementation details:
- Introduced `nexus_wasm.cpp` which defines Emscripten bindings (using
  Embind) for core simulation controls, including stepping time,
  node creation, topology orchestration, and state manipulation.
- Implemented a `WasmObserver` and a global event queue to capture
  simulation events (node state changes, link updates, packet events)
  and expose them to JavaScript via a polling mechanism (`pollEvent`).
- Updated the CMake build system to support the `EMSCRIPTEN` platform,
  configuring specific linker options for ES6 module export,
  modularization, and memory growth.
- Enhanced `build.sh` to allow targeting WASM via `emcmake`.
- Guarded file-system-dependent operations in `nexus_pcap.cpp` and
  adjusted `nexus_core.cpp` to handle WASM-specific constraints where
  standard I/O or multiple observers might not be applicable.
- Added `test_wasm_bindings.mjs`, a Node.js-based smoke test that
  verifies the integrity of the WASM bindings and event pipeline.
- Integrated `nexus-wasm-tests` into the GitHub Actions workflow to
  ensure continuous verification of the WASM build and functionality.
2026-04-17 15:25:28 -05:00

294 lines
10 KiB
C++

/*
* Copyright (c) 2026, The OpenThread Authors.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the copyright holder nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include "nexus_pcap.hpp"
#include "nexus_utils.hpp"
#include "common/clearable.hpp"
#include "common/code_utils.hpp"
#include "common/encoding.hpp"
namespace ot {
namespace Nexus {
#ifndef __EMSCRIPTEN__
OT_TOOL_PACKED_BEGIN
struct Epb
{
uint32_t mBlockType;
uint32_t mBlockTotalLength;
uint32_t mInterfaceId;
uint32_t mTimestampHigh;
uint32_t mTimestampLow;
uint32_t mCapturedLen;
uint32_t mOriginalLen;
} OT_TOOL_PACKED_END;
Pcap::Pcap(void)
: mFile(nullptr)
{
}
Pcap::~Pcap(void) { Close(); }
void Pcap::Open(const char *aFilename)
{
OT_TOOL_PACKED_BEGIN
struct Shb
{
uint32_t mBlockType;
uint32_t mBlockTotalLength;
uint32_t mByteOrderMagic;
uint16_t mVersionMajor;
uint16_t mVersionMinor;
uint64_t mSectionLength;
uint32_t mBlockTotalLength2;
} OT_TOOL_PACKED_END shb;
OT_TOOL_PACKED_BEGIN
struct Idb
{
uint32_t mBlockType;
uint32_t mBlockTotalLength;
uint16_t mLinkType;
uint16_t mReserved;
uint32_t mSnapLen;
uint32_t mBlockTotalLength2;
} OT_TOOL_PACKED_END idb;
Close();
mFile = fopen(aFilename, "wb");
VerifyOrExit(mFile != nullptr);
ClearAllBytes(shb);
shb.mBlockType = LittleEndian::HostSwap(kPcapngShbType);
shb.mBlockTotalLength = LittleEndian::HostSwap(static_cast<uint32_t>(sizeof(shb)));
shb.mByteOrderMagic = LittleEndian::HostSwap(kPcapngByteOrderMagic);
shb.mVersionMajor = LittleEndian::HostSwap(kPcapngVersionMajor);
shb.mVersionMinor = LittleEndian::HostSwap(kPcapngVersionMinor);
shb.mSectionLength = 0xffffffffffffffffULL;
shb.mBlockTotalLength2 = shb.mBlockTotalLength;
VerifyOrExit(fwrite(&shb, sizeof(shb), 1, mFile) == 1, Close());
ClearAllBytes(idb);
idb.mBlockType = LittleEndian::HostSwap(kPcapngIdbType);
idb.mBlockTotalLength = LittleEndian::HostSwap(static_cast<uint32_t>(sizeof(idb)));
idb.mLinkType = LittleEndian::HostSwap(static_cast<uint16_t>(kPcapngLinkTypeIeee802154));
idb.mSnapLen = LittleEndian::HostSwap(kPcapngSnapLen);
idb.mBlockTotalLength2 = idb.mBlockTotalLength;
VerifyOrExit(fwrite(&idb, sizeof(idb), 1, mFile) == 1, Close());
ClearAllBytes(idb);
idb.mBlockType = LittleEndian::HostSwap(kPcapngIdbType);
idb.mBlockTotalLength = LittleEndian::HostSwap(static_cast<uint32_t>(sizeof(idb)));
idb.mLinkType = LittleEndian::HostSwap(static_cast<uint16_t>(kPcapngLinkTypeEthernet));
idb.mSnapLen = LittleEndian::HostSwap(kPcapngSnapLen);
idb.mBlockTotalLength2 = idb.mBlockTotalLength;
VerifyOrExit(fwrite(&idb, sizeof(idb), 1, mFile) == 1, Close());
VerifyOrExit(fflush(mFile) == 0, Close());
exit:
return;
}
void Pcap::Close(void)
{
VerifyOrExit(mFile != nullptr);
fclose(mFile);
mFile = nullptr;
exit:
return;
}
void Pcap::WriteFrame(const otRadioFrame &aFrame, uint64_t aTimeUs)
{
Epb epb;
OT_TOOL_PACKED_BEGIN
struct TapHeader
{
uint16_t mVersion;
uint16_t mLength;
} OT_TOOL_PACKED_END tapHeader;
uint32_t tapLength;
uint32_t packetLen;
uint32_t paddedLen;
uint32_t blockTotalLength;
uint32_t padding = 0;
VerifyOrExit(mFile != nullptr);
tapLength = sizeof(tapHeader) + 8 + 8;
// Note: tshark expects specific lengths for TLVs.
// FCS TLV: Type(2), Length(2), Value(1), Padding(3) -> Total 8 bytes. Value length = 1.
// Channel TLV: Type(2), Length(2), Channel(2), Page(1), Padding(1) -> Total 8 bytes. Value length = 3.
packetLen = tapLength + aFrame.mLength;
paddedLen = (packetLen + 3) & ~3u;
blockTotalLength = sizeof(epb) + paddedLen + sizeof(uint32_t);
ClearAllBytes(epb);
epb.mBlockType = LittleEndian::HostSwap(kPcapngEpbType);
epb.mBlockTotalLength = LittleEndian::HostSwap(blockTotalLength);
epb.mInterfaceId = LittleEndian::HostSwap(0u);
epb.mTimestampHigh = LittleEndian::HostSwap(static_cast<uint32_t>(aTimeUs >> 32));
epb.mTimestampLow = LittleEndian::HostSwap(static_cast<uint32_t>(aTimeUs & 0xffffffff));
epb.mCapturedLen = LittleEndian::HostSwap(packetLen);
epb.mOriginalLen = LittleEndian::HostSwap(packetLen);
VerifyOrExit(fwrite(&epb, sizeof(epb), 1, mFile) == 1, Close());
ClearAllBytes(tapHeader);
tapHeader.mVersion = LittleEndian::HostSwap(static_cast<uint16_t>(kTapVersion));
tapHeader.mLength = LittleEndian::HostSwap(static_cast<uint16_t>(tapLength));
VerifyOrExit(fwrite(&tapHeader, sizeof(tapHeader), 1, mFile) == 1, Close());
// FCS TLV
uint8_t fcsTlv[8];
ClearAllBytes(fcsTlv);
LittleEndian::WriteUint16(kTapFcsType, &fcsTlv[0]);
LittleEndian::WriteUint16(kTapFcsLength, &fcsTlv[2]);
fcsTlv[4] = kTapFcsValue;
VerifyOrExit(fwrite(fcsTlv, sizeof(fcsTlv), 1, mFile) == 1, Close());
// Channel TLV
uint8_t channelTlv[8];
ClearAllBytes(channelTlv);
LittleEndian::WriteUint16(kTapChannelType, &channelTlv[0]);
LittleEndian::WriteUint16(kTapChannelLength, &channelTlv[2]);
LittleEndian::WriteUint16(aFrame.mChannel, &channelTlv[4]);
channelTlv[6] = kTapChannelPage;
VerifyOrExit(fwrite(channelTlv, sizeof(channelTlv), 1, mFile) == 1, Close());
VerifyOrExit(fwrite(aFrame.mPsdu, aFrame.mLength, 1, mFile) == 1, Close());
if (paddedLen > packetLen)
{
VerifyOrExit(fwrite(&padding, paddedLen - packetLen, 1, mFile) == 1, Close());
}
VerifyOrExit(fwrite(&epb.mBlockTotalLength, sizeof(epb.mBlockTotalLength), 1, mFile) == 1, Close());
VerifyOrExit(fflush(mFile) == 0, Close());
exit:
return;
}
void Pcap::WritePacket(const InfraIf::LinkLayerAddress &aSrcAddr,
const InfraIf::LinkLayerAddress &aDstAddr,
const uint8_t *aBuffer,
uint16_t aLength,
uint64_t aTimeUs)
{
Epb epb;
OT_TOOL_PACKED_BEGIN
struct EthernetHeader
{
uint8_t mDst[6];
uint8_t mSrc[6];
uint16_t mType;
} OT_TOOL_PACKED_END;
EthernetHeader ethHeader;
uint32_t packetLen;
uint32_t paddedLen;
uint32_t blockTotalLength;
uint32_t padding = 0;
VerifyOrExit(mFile != nullptr);
ClearAllBytes(ethHeader);
VerifyOrQuit(aDstAddr.GetLength() >= sizeof(ethHeader.mDst),
"Destination MAC address length is less than expected");
memcpy(ethHeader.mDst, aDstAddr.GetBytes(), sizeof(ethHeader.mDst));
VerifyOrQuit(aSrcAddr.GetLength() >= sizeof(ethHeader.mSrc), "Source MAC address length is less than expected");
memcpy(ethHeader.mSrc, aSrcAddr.GetBytes(), sizeof(ethHeader.mSrc));
ethHeader.mType = BigEndian::HostSwap16(kEtherTypeIPv6);
packetLen = sizeof(ethHeader) + aLength;
paddedLen = (packetLen + 3) & ~3u;
blockTotalLength = sizeof(epb) + paddedLen + sizeof(uint32_t);
ClearAllBytes(epb);
epb.mBlockType = LittleEndian::HostSwap(kPcapngEpbType);
epb.mBlockTotalLength = LittleEndian::HostSwap(blockTotalLength);
epb.mInterfaceId = LittleEndian::HostSwap(1u); // Use Interface ID 1 for Ethernet
epb.mTimestampHigh = LittleEndian::HostSwap(static_cast<uint32_t>(aTimeUs >> 32));
epb.mTimestampLow = LittleEndian::HostSwap(static_cast<uint32_t>(aTimeUs & 0xffffffff));
epb.mCapturedLen = LittleEndian::HostSwap(packetLen);
epb.mOriginalLen = LittleEndian::HostSwap(packetLen);
VerifyOrExit(fwrite(&epb, sizeof(epb), 1, mFile) == 1, Close());
VerifyOrExit(fwrite(&ethHeader, sizeof(ethHeader), 1, mFile) == 1, Close());
VerifyOrExit(fwrite(aBuffer, aLength, 1, mFile) == 1, Close());
if (paddedLen > packetLen)
{
VerifyOrExit(fwrite(&padding, paddedLen - packetLen, 1, mFile) == 1, Close());
}
VerifyOrExit(fwrite(&epb.mBlockTotalLength, sizeof(epb.mBlockTotalLength), 1, mFile) == 1, Close());
VerifyOrExit(fflush(mFile) == 0, Close());
exit:
return;
}
#else // __EMSCRIPTEN__
Pcap::Pcap(void) {}
Pcap::~Pcap(void) {}
void Pcap::Open(const char *) {}
void Pcap::Close(void) {}
void Pcap::WriteFrame(const otRadioFrame &, uint64_t) {}
void Pcap::WritePacket(const InfraIf::LinkLayerAddress &,
const InfraIf::LinkLayerAddress &,
const uint8_t *,
uint16_t,
uint64_t)
{
}
#endif // __EMSCRIPTEN__
} // namespace Nexus
} // namespace ot