Compare commits

..

20 Commits

Author SHA1 Message Date
Nicholas Dudfield
dd616d807f Merge remote-tracking branch 'origin/dev' into coverage-llm 2026-06-17 14:35:13 +07:00
Nicholas Dudfield
98eb3be7af Merge remote-tracking branch 'origin/dev' into coverage-llm
# Conflicts:
#	.codecov.yml
2026-06-04 10:31:58 +07:00
Nicholas Dudfield
273273d7a2 ci: relax patch coverage for PeerImp 2026-05-06 16:27:15 +07:00
Nicholas Dudfield
c149351ccf ci: bump apt retries to 5 to handle ppa.launchpadcontent.net flakes 2026-05-05 10:48:13 +07:00
Nicholas Dudfield
436a0d5540 fix(ci): scope conan_deps_cxxflags per Conan package pattern
prior shape (bare json list -> 'tools.build:cxxflags=[...]') leaked into
the consumer/rippled build via the conan-generated toolchain
(CMAKE_CXX_FLAGS_INIT). on macOS, where the build action doesn't
override CMAKE_CXX_FLAGS, this would silently apply the workaround flag
to rippled itself.

reshape the input to a json object keyed by Conan package pattern:
  {"grpc/*":["-Wno-..."]}

action emits package-pattern scoped lines:
  grpc/*:tools.build:cxxflags=["-Wno-..."]

flags only apply while building the matching dependency. consumer
toolchain stays clean. python validator rejects the consumer pattern
('&') and malformed shapes.
2026-05-05 10:13:59 +07:00
Nicholas Dudfield
3834ec5997 ci: undisable workflows, drop gcc-13 coverage row, keep clang-20 llvm-cov as sole codecov reporter
- restore the 8 .yml.disabled workflows back to .yml
- drop the gcc-13 gcov coverage matrix row; native clang-20 llvm-cov is
  now the only codecov reporter
- update minimal_matrix indices to match
- remove the TEMP narrowing block that filtered to just the llvm-cov row
- log line says '7 configs (build x6 + clang-20 llvm-cov coverage)' to
  reflect the new shape
2026-05-05 09:44:48 +07:00
Nicholas Dudfield
6b3fb5ea14 ci: dedupe conan_deps_cxxflags in macOS workflow row
prior turn-of-events created two identical conan_deps_cxxflags entries
in the same matrix row from overlapping restore commits. keep one.
2026-05-05 09:41:18 +07:00
Nicholas Dudfield
83a6d14f7a ci: restore matrix-row comments explaining clang-20 + grpc workaround 2026-05-05 09:40:16 +07:00
Nicholas Dudfield
10fbafe996 fix(ci): restore conan_deps_cxxflags on macOS workflow
reverts part of f8a30c528d - removing this line dropped the apple-clang
grpc workaround that the dependencies action used to apply
unconditionally before the matrix-driven refactor. the .disabled rename
is itself a TEMP measure being reverted before merge, so the field needs
to be in place when the macOS workflow comes back.
2026-05-05 09:36:29 +07:00
Nicholas Dudfield
0342badb5d Revert "chore(cov): drop ... .disabled file edit" (partial)
restore the conan_deps_cxxflags addition to xahau-ga-macos.yml.disabled.
prior commit framed it as dead code, but the .disabled rename is itself
a TEMP measure being reverted before merge - the field is needed for the
macOS workflow to keep building grpc 1.50.1 once it's re-enabled, since
the unconditional workaround was removed from the dependencies action.
2026-05-05 09:27:01 +07:00
Nicholas Dudfield
f8a30c528d chore(cov): drop dead BASE_DIRECTORY arg + revert .disabled file edit
- CodeCoverageLLVM.cmake: BASE_DIRECTORY was parsed but never used
  (no analog to gcovr's -r in the llvm-cov commands we emit). dead arg.
- xahau-ga-macos.yml.disabled: revert the conan_deps_cxxflags addition.
  the file is disabled so the edit was bit-rotting in unreachable code.
  whoever revives the macOS workflow can wire up the field then.
2026-05-05 09:25:30 +07:00
Nicholas Dudfield
fe162a99a9 fix(cov): tighten coverage_tool=llvm validation + macOS tool discovery
two bugs caught in review:

- RippledSettings.cmake: the clang-only guard for coverage_tool=llvm ran
  before coverage_test could auto-enable coverage. so
  '-Dcoverage_tool=llvm -Dcoverage_test=Foo' on a gcc build slipped past
  the guard and produced an instrumentation/tool mismatch. move the
  guard after the auto-enable block.

- CodeCoverageLLVM.cmake: _find_llvm_cov_tools unconditionally
  preferred xcrun's tools on APPLE, which on a homebrew clang-N build
  would pair the user's clang with xcode's llvm-cov - exactly the
  version mismatch the helper is trying to avoid. gate the xcrun branch
  on CMAKE_CXX_COMPILER_ID == AppleClang.
2026-05-05 09:24:20 +07:00
Nicholas Dudfield
132bcf6e57 Merge remote-tracking branch 'origin/dev' into coverage-llm 2026-05-05 09:05:34 +07:00
Nicholas Dudfield
5b4a6703ea ci(deps): replace hardcoded grpc workaround with matrix-driven conan_deps_cxxflags
new dependencies action input `conan_deps_cxxflags` (json list, default
'[]') drives `tools.build:cxxflags` in the conan profile. clearly named
to indicate the flags only apply to conan dependency builds, not the
rippled build itself.

removes the per-os/per-compiler hardcoded workaround (linux clang
conditional + unconditional macOS block) that used to set the same flag
in two places kept-in-sync by hand.

call sites:
- xahau-ga-nix.yml: clang-20 coverage row gets the
  -Wno-missing-template-arg-list-after-template-kw workaround for grpc 1.50.1
- xahau-ga-macos.yml.disabled: same flag plumbed through (preserves
  prior behaviour when re-enabled)

drop the workaround entries when grpc is bumped past the fix.
2026-05-05 08:57:30 +07:00
Nicholas Dudfield
51cd3ddf25 fix(cov): use absolute binary path in llvm-cov run command
bare 'rippled' wasn't on PATH and the build dir isn't '.', so the
profile-collection step failed with 'No such file or directory'.
splice the resolved absolute path back into Cov_EXECUTABLE before
the run command consumes it.
2026-04-30 15:53:45 +07:00
Nicholas Dudfield
cf244334d3 ci(temp): disable all workflows except Nix GA on this branch
renames every non-nix workflow to .yml.disabled so they stop firing on
PR pushes while we iterate on the llvm-cov coverage row. MUST be
reverted before merging to dev.
2026-04-30 15:25:07 +07:00
Nicholas Dudfield
43a37afb51 ci(deps): apply Wno-missing-template-arg-list workaround to Linux clang
grpc 1.50.1 hits -Werror=missing-template-arg-list-after-template-kw on
clang-19+. macOS clang already had this workaround in the conan profile;
mirror it to the Linux branch, gated on compiler==clang.
2026-04-30 15:20:03 +07:00
Nicholas Dudfield
8d609f9cf3 ci: actually narrow matrix to just the llvm-cov row
prior guard checked base_ref against ['dev','candidate','release'] which
excluded the very PR we're iterating on. drop the guard - this branch
is for iteration only and must be reverted before merging anyway.
2026-04-30 15:15:46 +07:00
Nicholas Dudfield
94a62f5572 ci: drop coverage-llm from push triggers, PR trigger covers it 2026-04-30 15:13:47 +07:00
Nicholas Dudfield
7b8d671f52 ci: add -Dcoverage_tool=gcov|llvm option, wire native llvm-cov in matrix
introduces native llvm source-based coverage as an alternative to the
existing gcov + gcovr pipeline. driven by a new -Dcoverage_tool cache
variable (default 'gcov' for backwards compat; 'llvm' enables
-fprofile-instr-generate / -fcoverage-mapping + llvm-profdata + llvm-cov).

cmake:
- RippledSettings.cmake: add coverage_tool with validation
- RippledInterface.cmake: split coverage compile/link flags by tool
- RippledCov.cmake: dispatch to the new helper when tool=llvm
- CodeCoverageLLVM.cmake (new): setup_target_for_coverage_llvm() driving
  profraw -> profdata -> export with format-aware output (lcov/json/txt/html)

ci:
- new clang-20 llvm-cov coverage matrix row (apt.llvm.org bootstrap for
  clang >= 19 since 24.04 default repos cap at clang-18)
- conditional install of llvm-N vs gcovr based on coverage_tool
- artifact + codecov upload generalised to coverage.lcov | coverage.xml
- coverage cmake-args switched to *_FLAGS_DEBUG so the build action's
  stdlib flag isn't clobbered

temp (revert before merging to dev):
- matrix narrowed to just the llvm-cov row on non-main refs so we can
  iterate without burning runners
- 'coverage-llm' added to push trigger for direct-push CI runs
2026-04-30 15:07:52 +07:00
30 changed files with 622 additions and 1611 deletions

View File

@@ -19,6 +19,15 @@ coverage:
default:
target: auto
threshold: 2%
paths:
# PeerImp is historically hard to exercise in the current unit-test
# harness. Keep this list narrow; new testable code should remain
# covered by the default patch gate.
- "!src/xrpld/overlay/detail/PeerImp.cpp"
historically-untested:
target: 0%
paths:
- "src/xrpld/overlay/detail/PeerImp.cpp"
changes: false
github_checks:

View File

@@ -50,6 +50,10 @@ inputs:
options:
- libstdcxx
- libcxx
conan_deps_cxxflags:
description: 'Extra cxxflags applied to Conan dependency package builds only (NOT the rippled build). JSON object keyed by Conan package pattern, e.g. {"grpc/*":["-Wno-foo"]}. Maps to <pattern>:tools.build:cxxflags.'
required: false
default: '{}'
outputs:
cache-hit:
@@ -81,6 +85,8 @@ runs:
- name: Configure Conan
shell: bash
env:
CONAN_DEPS_CXXFLAGS: ${{ inputs.conan_deps_cxxflags }}
run: |
# Create the default profile directory if it doesn't exist
mkdir -p ~/.conan2/profiles
@@ -105,7 +111,14 @@ runs:
os=${{ inputs.os }}
EOF
# Add buildenv and conf sections for Linux (not needed for macOS)
# [buildenv] + [conf] sections.
# Linux pins compiler executables; macOS uses the system toolchain.
# conan_deps_cxxflags (matrix-driven) optionally adds package-pattern
# scoped tools.build:cxxflags for Conan dependency builds only - typically
# grpc workarounds for newer clang's stricter diagnostics. Because these
# are profile-pattern scoped (e.g. grpc/*:...), they do NOT affect the
# consumer/rippled toolchain generated for the main build.
NEED_CONF=0
if [ "${{ inputs.os }}" = "Linux" ] && [ -n "${{ inputs.cc }}" ]; then
cat >> ~/.conan2/profiles/default <<EOF
@@ -116,16 +129,38 @@ runs:
[conf]
tools.build:compiler_executables={"c": "/usr/bin/${{ inputs.cc }}", "cpp": "/usr/bin/${{ inputs.cxx }}"}
EOF
NEED_CONF=1
fi
# Add macOS-specific conf if needed
if [ "${{ inputs.os }}" = "Macos" ]; then
cat >> ~/.conan2/profiles/default <<EOF
if [ -n "${CONAN_DEPS_CXXFLAGS}" ] && [ "${CONAN_DEPS_CXXFLAGS}" != "{}" ]; then
CONAN_DEPS_CXXFLAGS_LINES="$(python3 - <<'PY'
import json
import os
import sys
[conf]
# Workaround for gRPC with newer Apple Clang
tools.build:cxxflags=["-Wno-missing-template-arg-list-after-template-kw"]
EOF
raw = os.environ["CONAN_DEPS_CXXFLAGS"]
data = json.loads(raw)
if not isinstance(data, dict):
sys.exit("conan_deps_cxxflags must be a JSON object like {\"grpc/*\": [\"-Wno-...\"]}")
for pattern, flags in data.items():
if not isinstance(pattern, str) or not pattern:
sys.exit("conan_deps_cxxflags keys must be non-empty Conan package patterns")
if pattern == "&":
sys.exit("conan_deps_cxxflags must target dependency package patterns, not the consumer (&)")
if not isinstance(flags, list) or not all(isinstance(flag, str) for flag in flags):
sys.exit(f"{pattern}: cxxflags must be a JSON string list")
if flags:
print(f"{pattern}:tools.build:cxxflags={json.dumps(flags, separators=(',', ':'))}")
PY
)"
if [ -n "${CONAN_DEPS_CXXFLAGS_LINES}" ] && [ "$NEED_CONF" = "0" ]; then
echo "" >> ~/.conan2/profiles/default
echo "[conf]" >> ~/.conan2/profiles/default
fi
if [ -n "${CONAN_DEPS_CXXFLAGS_LINES}" ]; then
printf '%s\n' "${CONAN_DEPS_CXXFLAGS_LINES}" >> ~/.conan2/profiles/default
fi
fi
# Display profile for verification

View File

@@ -107,6 +107,9 @@ jobs:
compiler: apple-clang
compiler_version: ${{ steps.detect-compiler.outputs.compiler_version }}
stdlib: libcxx
# grpc 1.50.1 trips clang-19+ -Werror=missing-template-arg-list-after-template-kw
# on Apple Clang. Drop when grpc is bumped past the fix.
conan_deps_cxxflags: '{"grpc/*":["-Wno-missing-template-arg-list-after-template-kw"]}'
- name: Build
uses: ./.github/actions/xahau-ga-build

View File

@@ -72,15 +72,26 @@ jobs:
"job_type": "build"
},
{
"compiler_id": "gcc-13-libstdcxx",
"compiler": "gcc",
"cc": "gcc-13",
"cxx": "g++-13",
"gcov": "gcov-13",
"compiler_version": 13,
"stdlib": "default",
# Latest stable Clang for the most accurate source-based
# coverage mapping (newer language features, fewer bugs in
# llvm-cov region inference). Pulled from apt.llvm.org since
# Ubuntu 24.04 default repos cap at clang-18.
"compiler_id": "clang-20-libcxx",
"compiler": "clang",
"cc": "clang-20",
"cxx": "clang++-20",
"compiler_version": 20,
"stdlib": "libcxx",
"configuration": "Debug",
"job_type": "coverage"
"job_type": "coverage",
"coverage_tool": "llvm",
"coverage_format": "lcov",
# grpc 1.50.1 uses `Foo::template Bar(...)` without an
# angle-bracket arg list; clang-19+ promoted that to
# -Werror. Drop when grpc is bumped past the fix.
"conan_deps_cxxflags": {
"grpc/*": ["-Wno-missing-template-arg-list-after-template-kw"]
}
},
{
"compiler_id": "clang-14-libstdcxx-gcc11",
@@ -131,7 +142,7 @@ jobs:
# Minimal matrix for PRs and feature branches
minimal_matrix = [
full_matrix[1], # gcc-13 (middle-ground gcc)
full_matrix[2], # gcc-13 coverage
full_matrix[2], # clang-20 llvm-cov coverage
full_matrix[3] # clang-14 (mature, stable clang)
]
@@ -207,9 +218,9 @@ jobs:
# Select the appropriate matrix
if use_full:
if force_full:
print(f"Using FULL matrix (7 configs) - forced by [ci-nix-full-matrix] tag")
print(f"Using FULL matrix (7 configs (build x6 + clang-20 llvm-cov coverage)) - forced by [ci-nix-full-matrix] tag")
else:
print(f"Using FULL matrix (7 configs) - targeting main branch")
print(f"Using FULL matrix (7 configs (build x6 + clang-20 llvm-cov coverage)) - targeting main branch")
matrix = full_matrix
else:
print(f"Using MINIMAL matrix (3 configs) - feature branch/PR")
@@ -257,9 +268,25 @@ jobs:
- name: Install build dependencies
run: |
# Bump apt's default 3 retries; papers over short upstream blips
# like the recurring ppa.launchpadcontent.net outages.
echo 'Acquire::Retries "5";' > /etc/apt/apt.conf.d/80-retries
apt-get update
apt-get install -y software-properties-common
add-apt-repository ppa:ubuntu-toolchain-r/test -y
# apt.llvm.org for Clang versions newer than what Ubuntu 24.04 ships
# (24.04 default repos cap at clang-18). The bootstrap script adds
# the LLVM apt source for the requested version and runs apt-get update.
if [ "${{ matrix.compiler }}" = "clang" ] && [ "${{ matrix.compiler_version }}" -ge 19 ]; then
apt-get install -y wget gnupg lsb-release
wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh
chmod +x /tmp/llvm.sh
# `all` installs clang + libllvm + lldb + lld + the llvm-N package
# (which provides llvm-profdata-N / llvm-cov-N for coverage runs).
/tmp/llvm.sh ${{ matrix.compiler_version }} all
fi
apt-get update
apt-get install -y git python3 python-is-python3 pipx
pipx ensurepath
@@ -332,10 +359,16 @@ jobs:
pipx install "conan>=2.0,<3"
echo "$HOME/.local/bin" >> $GITHUB_PATH
# Install gcovr for coverage jobs
# Install coverage tooling
if [ "${{ matrix.job_type }}" = "coverage" ]; then
pipx install "gcovr>=7,<9"
apt-get install -y curl lcov
if [ "${{ matrix.coverage_tool }}" = "llvm" ]; then
# Native LLVM source-based coverage: llvm-profdata + llvm-cov.
# The clang-N package doesn't pull these in; the llvm-N package does.
apt-get install -y "llvm-${{ matrix.compiler_version }}"
else
pipx install "gcovr>=7,<9"
fi
fi
- name: Check environment
@@ -348,10 +381,15 @@ jobs:
which ${{ matrix.cxx }} && ${{ matrix.cxx }} --version || echo "${{ matrix.cxx }} not found"
which ccache && ccache --version || echo "ccache not found"
# Check gcovr for coverage jobs
# Check coverage tooling
if [ "${{ matrix.job_type }}" = "coverage" ]; then
which gcov && gcov --version || echo "gcov not found"
which gcovr && gcovr --version || echo "gcovr not found"
if [ "${{ matrix.coverage_tool }}" = "llvm" ]; then
which "llvm-profdata-${{ matrix.compiler_version }}" && "llvm-profdata-${{ matrix.compiler_version }}" --version || echo "llvm-profdata not found"
which "llvm-cov-${{ matrix.compiler_version }}" && "llvm-cov-${{ matrix.compiler_version }}" --version || echo "llvm-cov not found"
else
which gcov && gcov --version || echo "gcov not found"
which gcovr && gcovr --version || echo "gcovr not found"
fi
fi
echo "---- Full Environment ----"
@@ -378,6 +416,7 @@ jobs:
cc: ${{ matrix.cc }}
cxx: ${{ matrix.cxx }}
stdlib: ${{ matrix.stdlib }}
conan_deps_cxxflags: ${{ matrix.conan_deps_cxxflags && toJson(matrix.conan_deps_cxxflags) || '{}' }}
gha_cache_enabled: 'false' # Disable caching for self hosted runner
- name: Build
@@ -410,8 +449,9 @@ jobs:
cache_version: ${{ env.CACHE_VERSION }}
main_branch: ${{ env.MAIN_BRANCH_NAME }}
stdlib: ${{ matrix.stdlib }}
# Coverage builds are slower due to instrumentation; use fewer parallel jobs to avoid flakiness
cmake-args: '-Dcoverage=ON -Dcoverage_format=xml -Dcoverage_test_parallelism=$(($(nproc)/2)) -DCODE_COVERAGE_VERBOSE=ON -DCMAKE_CXX_FLAGS="-O0" -DCMAKE_C_FLAGS="-O0"'
# Coverage builds are slower due to instrumentation; use fewer parallel jobs to avoid flakiness.
# Use *_FLAGS_DEBUG so the build action's stdlib flag (e.g. -stdlib=libc++) in CMAKE_CXX_FLAGS isn't clobbered.
cmake-args: '-Dcoverage=ON -Dcoverage_tool=${{ matrix.coverage_tool }} -Dcoverage_format=${{ matrix.coverage_format }} -Dcoverage_test_parallelism=$(($(nproc)/2)) -DCODE_COVERAGE_VERBOSE=ON -DCMAKE_CXX_FLAGS_DEBUG="-g -O0" -DCMAKE_C_FLAGS_DEBUG="-g -O0"'
cmake-target: 'coverage'
ccache_max_size: '100G'
@@ -443,22 +483,26 @@ jobs:
- name: Move coverage report
if: matrix.job_type == 'coverage'
shell: bash
env:
COVERAGE_FILE: ${{ matrix.coverage_tool == 'llvm' && 'coverage.lcov' || 'coverage.xml' }}
run: |
mv "${{ env.build_dir }}/coverage.xml" ./
mv "${{ env.build_dir }}/${COVERAGE_FILE}" ./
echo "COVERAGE_FILE=${COVERAGE_FILE}" >> "$GITHUB_ENV"
- name: Archive coverage report
if: matrix.job_type == 'coverage'
uses: actions/upload-artifact@v4
with:
name: coverage.xml
path: coverage.xml
name: ${{ env.COVERAGE_FILE }}-${{ matrix.compiler_id }}
path: ${{ env.COVERAGE_FILE }}
retention-days: 30
- name: Upload coverage report
if: matrix.job_type == 'coverage'
uses: codecov/codecov-action@v5
with:
files: coverage.xml
files: ${{ env.COVERAGE_FILE }}
flags: ${{ matrix.coverage_tool }}
fail_ci_if_error: true
disable_search: true
verbose: true

View File

@@ -124,8 +124,7 @@ find_package(date REQUIRED)
find_package(xxHash REQUIRED)
find_package(magic_enum REQUIRED)
include(deps/WasmEdge)
include(deps/Wasmtime)
include(deps/WasmEdge)
if(TARGET nudb::core)
set(nudb nudb::core)
elseif(TARGET NuDB::nudb)

View File

@@ -95,16 +95,8 @@ if [[ "$4" == "" ]]; then
echo "Non GH, local building, no Action runner magic"
else
# GH Action, runner
if [[ "$(git rev-parse --abbrev-ref HEAD)" == "release" ]]; then
echo "building on the release branch... placing it in builds/candidate"
mkdir /data/builds/candidate
cp /io/release-build/xahaud /data/builds/candidate/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4
cp /io/release-build/release.info /data/builds/candidate/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4.releaseinfo
else
echo "building non-release branch, placing it in builds root"
cp /io/release-build/xahaud /data/builds/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4
cp /io/release-build/release.info /data/builds/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4.releaseinfo
fi
cp /io/release-build/xahaud /data/builds/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4
cp /io/release-build/release.info /data/builds/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4.releaseinfo
echo "Published build to: http://build.xahau.tech/"
echo $(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4
fi

View File

@@ -0,0 +1,156 @@
#[===================================================================[
Native LLVM source-based code coverage helper.
Drives the -fprofile-instr-generate / -fcoverage-mapping pipeline:
1. Run instrumented binary with LLVM_PROFILE_FILE=...%m-%p.profraw
2. llvm-profdata merge -sparse -> coverage.profdata
3. llvm-cov export/show/report -> final report
Output filename per coverage_format:
lcov -> coverage.lcov
json -> coverage.json
txt | text -> coverage.txt
html | html-details -> <NAME>/index.html
#]===================================================================]
include(CMakeParseArguments)
# Locate llvm-profdata / llvm-cov, preferring versioned variants matching the
# Clang we're building with so we don't accidentally pair clang-20 with
# llvm-cov-14 (profile format mismatch -> hard failure).
function(_find_llvm_cov_tools)
if(LLVM_PROFDATA_PATH AND LLVM_COV_PATH)
return()
endif()
string(REGEX MATCH "^[0-9]+" _major "${CMAKE_CXX_COMPILER_VERSION}")
set(_pd_names llvm-profdata)
set(_cov_names llvm-cov)
if(_major)
list(PREPEND _pd_names "llvm-profdata-${_major}")
list(PREPEND _cov_names "llvm-cov-${_major}")
endif()
# Only delegate to xcrun when the *compiler* is AppleClang. On macOS with
# Homebrew/system clang-N, xcrun would resolve to Xcode's llvm tools which
# could be a different version - exactly the mismatch we want to avoid.
if(APPLE AND CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
execute_process(COMMAND xcrun -f llvm-profdata
OUTPUT_VARIABLE _pd_xcrun OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET RESULT_VARIABLE _pd_rc)
if(_pd_rc EQUAL 0 AND _pd_xcrun)
set(LLVM_PROFDATA_PATH "${_pd_xcrun}" CACHE FILEPATH "llvm-profdata" FORCE)
endif()
execute_process(COMMAND xcrun -f llvm-cov
OUTPUT_VARIABLE _cov_xcrun OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET RESULT_VARIABLE _cov_rc)
if(_cov_rc EQUAL 0 AND _cov_xcrun)
set(LLVM_COV_PATH "${_cov_xcrun}" CACHE FILEPATH "llvm-cov" FORCE)
endif()
endif()
if(NOT LLVM_PROFDATA_PATH)
find_program(LLVM_PROFDATA_PATH NAMES ${_pd_names})
endif()
if(NOT LLVM_COV_PATH)
find_program(LLVM_COV_PATH NAMES ${_cov_names})
endif()
endfunction()
function(setup_target_for_coverage_llvm)
set(oneValueArgs NAME FORMAT)
set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES)
cmake_parse_arguments(Cov "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
_find_llvm_cov_tools()
if(NOT LLVM_PROFDATA_PATH)
message(FATAL_ERROR "llvm-profdata not found (needed for coverage_tool=llvm)")
endif()
if(NOT LLVM_COV_PATH)
message(FATAL_ERROR "llvm-cov not found (needed for coverage_tool=llvm)")
endif()
if(NOT Cov_FORMAT)
set(Cov_FORMAT lcov)
endif()
set(_profraw_dir "${PROJECT_BINARY_DIR}/${Cov_NAME}-profraw")
set(_profdata "${PROJECT_BINARY_DIR}/${Cov_NAME}.profdata")
# Resolve binary path: accept either an absolute path or a bare target name
# (resolved against PROJECT_BINARY_DIR). Splice the resolved path back into
# Cov_EXECUTABLE so the run command invokes it via absolute path - bare
# names aren't on PATH and the build dir isn't `.` either.
list(GET Cov_EXECUTABLE 0 _exec_name)
if(IS_ABSOLUTE "${_exec_name}")
set(_binary "${_exec_name}")
else()
set(_binary "${PROJECT_BINARY_DIR}/${_exec_name}")
list(REMOVE_AT Cov_EXECUTABLE 0)
list(PREPEND Cov_EXECUTABLE "${_binary}")
endif()
# llvm-cov takes a single -ignore-filename-regex; OR our excludes together.
set(_ignore_regex "")
foreach(EXC IN LISTS Cov_EXCLUDE)
if(_ignore_regex)
string(APPEND _ignore_regex "|")
endif()
string(APPEND _ignore_regex "${EXC}")
endforeach()
set(_filter "")
if(_ignore_regex)
set(_filter "-ignore-filename-regex='${_ignore_regex}'")
endif()
# Pick llvm-cov subcommand + output file for the requested format. Each
# branch builds a single shell command string that we'll hand to bash -c.
if(Cov_FORMAT STREQUAL "lcov")
set(_output "${PROJECT_BINARY_DIR}/coverage.lcov")
set(_report_sh "${LLVM_COV_PATH} export -instr-profile='${_profdata}' -format=lcov ${_filter} '${_binary}' > '${_output}'")
elseif(Cov_FORMAT STREQUAL "json")
set(_output "${PROJECT_BINARY_DIR}/coverage.json")
set(_report_sh "${LLVM_COV_PATH} export -instr-profile='${_profdata}' -format=text ${_filter} '${_binary}' > '${_output}'")
elseif(Cov_FORMAT STREQUAL "txt" OR Cov_FORMAT STREQUAL "text")
set(_output "${PROJECT_BINARY_DIR}/coverage.txt")
set(_report_sh "${LLVM_COV_PATH} report -instr-profile='${_profdata}' ${_filter} '${_binary}' > '${_output}'")
elseif(Cov_FORMAT STREQUAL "html" OR Cov_FORMAT STREQUAL "html-details")
set(_output "${PROJECT_BINARY_DIR}/${Cov_NAME}/index.html")
set(_report_sh "${LLVM_COV_PATH} show -instr-profile='${_profdata}' -format=html -output-dir='${PROJECT_BINARY_DIR}/${Cov_NAME}' ${_filter} '${_binary}'")
else()
message(FATAL_ERROR "coverage_tool=llvm: unsupported coverage_format '${Cov_FORMAT}' (use lcov|json|txt|html)")
endif()
set(_merge_sh "${LLVM_PROFDATA_PATH} merge -sparse -o '${_profdata}' '${_profraw_dir}'/*.profraw")
if(CODE_COVERAGE_VERBOSE)
message(STATUS "[coverage:llvm] binary: ${_binary}")
message(STATUS "[coverage:llvm] profraw: ${_profraw_dir}")
message(STATUS "[coverage:llvm] profdata: ${_profdata}")
message(STATUS "[coverage:llvm] format: ${Cov_FORMAT}")
message(STATUS "[coverage:llvm] output: ${_output}")
if(_ignore_regex)
message(STATUS "[coverage:llvm] ignore: ${_ignore_regex}")
endif()
message(STATUS "[coverage:llvm] merge: ${_merge_sh}")
message(STATUS "[coverage:llvm] report: ${_report_sh}")
endif()
# %m: hash of the binary, %p: pid. Wipe the dir up front so stale profraw
# files can't leak into a fresh merge.
add_custom_target(${Cov_NAME}
COMMAND ${CMAKE_COMMAND} -E rm -rf "${_profraw_dir}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${_profraw_dir}"
COMMAND ${CMAKE_COMMAND} -E env
"LLVM_PROFILE_FILE=${_profraw_dir}/rippled-%m-%p.profraw"
${Cov_EXECUTABLE} ${Cov_EXECUTABLE_ARGS}
COMMAND bash -c "${_merge_sh}"
COMMAND bash -c "${_report_sh}"
BYPRODUCTS ${_output}
WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
DEPENDS ${Cov_DEPENDENCIES}
VERBATIM
COMMENT "Running llvm-cov (${Cov_FORMAT}) -> ${_output}"
)
endfunction()

View File

@@ -58,7 +58,6 @@ target_link_libraries(xrpl.imports.main
OpenSSL::Crypto
Ripple::boost
wasmedge::wasmedge
wasmtime::wasmtime
Ripple::opts
Ripple::syslibs
absl::random_random

View File

@@ -11,6 +11,21 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
return()
endif()
if(coverage_tool STREQUAL "llvm")
include(CodeCoverageLLVM)
setup_target_for_coverage_llvm(
NAME coverage
FORMAT ${coverage_format}
EXECUTABLE rippled
EXECUTABLE_ARGS --unittest$<$<BOOL:${coverage_test}>:=${coverage_test}> --unittest-jobs ${coverage_test_parallelism} --quiet --unittest-log
EXCLUDE "src/test" "include/xrpl/beast/test" "include/xrpl/beast/unit_test" "${CMAKE_BINARY_DIR}/pb-xrpl.libpb"
DEPENDENCIES rippled
)
return()
endif()
# coverage_tool == "gcov" (default): existing gcovr-driven pipeline.
include(CodeCoverage)
# The instructions for these commands come from the `CodeCoverage` module,

View File

@@ -28,15 +28,17 @@ target_compile_options (opts
$<$<AND:$<BOOL:${is_gcc}>,$<COMPILE_LANGUAGE:CXX>>:-Wsuggest-override>
$<$<BOOL:${is_gcc}>:-Wno-maybe-uninitialized>
$<$<BOOL:${perf}>:-fno-omit-frame-pointer>
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${coverage}>>:-g --coverage -fprofile-abs-path>
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>>:-g --coverage>
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},gcov>>:-g --coverage -fprofile-abs-path>
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},gcov>>:-g --coverage>
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},llvm>>:-g -fprofile-instr-generate -fcoverage-mapping>
$<$<BOOL:${profile}>:-pg>
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${profile}>>:-p>)
target_link_libraries (opts
INTERFACE
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${coverage}>>:-g --coverage -fprofile-abs-path>
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>>:-g --coverage>
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},gcov>>:-g --coverage -fprofile-abs-path>
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},gcov>>:-g --coverage>
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},llvm>>:-g -fprofile-instr-generate -fcoverage-mapping>
$<$<BOOL:${profile}>:-pg>
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${profile}>>:-p>)

View File

@@ -29,13 +29,25 @@ if(is_gcc OR is_clang)
"Unit tests parallelism for the purpose of coverage report.")
set(coverage_format "html-details" CACHE STRING
"Output format of the coverage report.")
set(coverage_tool "gcov" CACHE STRING
"Coverage instrumentation tool: 'gcov' (default, gcc/clang via --coverage + gcovr) or 'llvm' (clang only, native source-based coverage via -fprofile-instr-generate).")
set_property(CACHE coverage_tool PROPERTY STRINGS "gcov" "llvm")
if(NOT coverage_tool MATCHES "^(gcov|llvm)$")
message(FATAL_ERROR "coverage_tool must be 'gcov' or 'llvm', got '${coverage_tool}'")
endif()
set(coverage_extra_args "" CACHE STRING
"Additional arguments to pass to gcovr.")
"Additional arguments to pass to gcovr (gcov tool only).")
set(coverage_test "" CACHE STRING
"On gcc & clang, the specific unit test(s) to run for coverage. Default is all tests.")
if(coverage_test AND NOT coverage)
set(coverage ON CACHE BOOL "gcc/clang only" FORCE)
endif()
# Validate after coverage_test may have flipped coverage on, otherwise
# `-Dcoverage_tool=llvm -Dcoverage_test=Foo` on gcc would silently slip
# past the Clang guard and produce a broken instrumentation combo.
if(coverage AND coverage_tool STREQUAL "llvm" AND NOT is_clang)
message(FATAL_ERROR "coverage_tool=llvm requires Clang (got ${CMAKE_CXX_COMPILER_ID})")
endif()
option(wextra "compile with extra gcc/clang warnings enabled" ON)
else()
set(profile OFF CACHE BOOL "gcc/clang only" FORCE)

View File

@@ -1 +0,0 @@
find_package(wasmtime REQUIRED)

View File

@@ -22,7 +22,6 @@ class Xrpl(ConanFile):
'unity': [True, False],
'xrpld': [True, False],
'with_wasmedge': [True, False],
'with_wasmtime': [True, False],
'tool_requires_b2': [True, False],
}
@@ -54,7 +53,6 @@ class Xrpl(ConanFile):
'unity': False,
'xrpld': False,
'with_wasmedge': True,
'with_wasmtime': True,
'tool_requires_b2': False,
'date/*:header_only': False,
@@ -123,8 +121,6 @@ class Xrpl(ConanFile):
if self.options.with_wasmedge:
self.requires('wasmedge/0.11.2@xahaud/stable')
if self.options.with_wasmtime:
self.requires('wasmtime/44.0.1@xahaud/stable')
if self.options.jemalloc:
self.requires('jemalloc/5.3.0')
if self.options.rocksdb:

View File

@@ -1,25 +0,0 @@
sources:
"44.0.1":
Macos:
"armv8":
"gcc":
url: "https://github.com/bytecodealliance/wasmtime/releases/download/v44.0.1/wasmtime-v44.0.1-aarch64-macos-c-api.tar.xz"
sha256: "5da6d09fa6340db6d7afc8b73d189eff4973825c9a6fa5e814d2f9b1ddfd8998"
"x86_64":
"gcc":
url: "https://github.com/bytecodealliance/wasmtime/releases/download/v44.0.1/wasmtime-v44.0.1-x86_64-macos-c-api.tar.xz"
sha256: ""
Linux:
"x86_64":
"gcc":
url: "https://github.com/bytecodealliance/wasmtime/releases/download/v44.0.1/wasmtime-v44.0.1-x86_64-linux-c-api.tar.xz"
sha256: "fe9b4bfa87724054cebcc09b83a17d5ded66a465054b77c8de0ebc166ad78d7a"
"armv8":
"gcc":
url: "https://github.com/bytecodealliance/wasmtime/releases/download/v44.0.1/wasmtime-v44.0.1-aarch64-linux-c-api.tar.xz"
sha256: ""
Windows:
"x86_64":
"Visual Studio":
url: "https://github.com/bytecodealliance/wasmtime/releases/download/v44.0.1/wasmtime-v44.0.1-x86_64-windows-c-api.zip"
sha256: ""

View File

@@ -1,87 +0,0 @@
from conan import ConanFile
from conan.tools.files import get, copy
from conan.errors import ConanInvalidConfiguration
import os
required_conan_version = ">=1.53.0"
class WasmtimeConan(ConanFile):
name = "wasmtime"
description = ("Wasmtime is a fast and secure runtime for WebAssembly. "
"It is a standalone wasm-only optimizing runtime for WebAssembly and WASI.")
license = "Apache-2.0"
url = "https://github.com/bytecodealliance/wasmtime"
homepage = "https://github.com/bytecodealliance/wasmtime"
topics = ("webassembly", "wasm", "wasi")
package_type = "static-library"
settings = "os", "arch", "compiler", "build_type"
@property
def _compiler_alias(self):
return {
"Visual Studio": "Visual Studio",
"msvc": "Visual Studio",
}.get(str(self.info.settings.compiler), "gcc")
def configure(self):
self.settings.compiler.rm_safe("libcxx")
self.settings.compiler.rm_safe("cppstd")
def validate(self):
try:
self.conan_data["sources"][self.version][str(self.settings.os)][str(self.settings.arch)][self._compiler_alias]
except KeyError:
raise ConanInvalidConfiguration(
f"Binaries for this combination of version/os/arch/compiler are not available "
f"({self.version}/{self.settings.os}/{self.settings.arch}/{self._compiler_alias})"
)
entry = self.conan_data["sources"][self.version][str(self.settings.os)][str(self.settings.arch)][self._compiler_alias]
if not entry.get("sha256"):
raise ConanInvalidConfiguration(
f"SHA256 not configured for {self.version}/{self.settings.os}/{self.settings.arch}/{self._compiler_alias}"
)
def package_id(self):
# Make binary compatible across compiler versions (since we're downloading prebuilt)
self.info.settings.rm_safe("compiler.version")
# Group compilers by their binary compatibility
compiler_name = str(self.info.settings.compiler)
if compiler_name in ["Visual Studio", "msvc"]:
self.info.settings.compiler = "Visual Studio"
else:
self.info.settings.compiler = "gcc"
def build(self):
# Download the prebuilt tarball
entry = self.conan_data["sources"][self.version][str(self.settings.os)][str(self.settings.arch)][self._compiler_alias]
get(self, url=entry["url"], sha256=entry["sha256"],
destination=self.source_folder, strip_root=True)
def package(self):
copy(self, pattern="*.h", dst=os.path.join(self.package_folder, "include"),
src=os.path.join(self.source_folder, "include"), keep_path=True)
srclibdir = os.path.join(self.source_folder, "lib")
dstlibdir = os.path.join(self.package_folder, "lib")
if self.settings.os == "Windows":
copy(self, pattern="wasmtime.lib", src=srclibdir, dst=dstlibdir, keep_path=False)
copy(self, pattern="wasmtime.dll", src=srclibdir, dst=os.path.join(self.package_folder, "bin"), keep_path=False)
elif self.settings.os == "Macos":
copy(self, pattern="libwasmtime.a", src=srclibdir, dst=dstlibdir, keep_path=False)
copy(self, pattern="libwasmtime.dylib", src=srclibdir, dst=dstlibdir, keep_path=False)
else:
copy(self, pattern="libwasmtime.a", src=srclibdir, dst=dstlibdir, keep_path=False)
copy(self, pattern="libwasmtime.so*", src=srclibdir, dst=dstlibdir, keep_path=False)
copy(self, pattern="LICENSE", src=self.source_folder, dst=os.path.join(self.package_folder, "licenses"), keep_path=False)
def package_info(self):
self.cpp_info.libs = ["wasmtime"]
if self.settings.os == "Windows":
self.cpp_info.system_libs += ["ws2_32", "bcrypt", "advapi32", "userenv", "ntdll", "shell32", "ole32"]
if self.settings.os in ["Linux", "FreeBSD"]:
self.cpp_info.system_libs += ["pthread", "dl", "m"]

View File

@@ -10,8 +10,6 @@
* were then used.
*/
#include <xrpl/hook/WasmTypes.h>
#define LPAREN (
#define RPAREN )
#define COMMA ,
@@ -70,90 +68,104 @@
#define SEP_uint64_t LPAREN uint64_t COMMA
#define SEP_int64_t LPAREN int64_t COMMA
// VAL_* : extract typed value from WasmValue in[] array (engine-agnostic)
#define VAL_uint32_t (uint32_t) in[_stack++].asI32()
#define VAL_int32_t (int32_t) in[_stack++].asI32()
#define VAL_uint64_t (uint64_t) in[_stack++].asI64()
#define VAL_int64_t (int64_t) in[_stack++].asI64()
#define VAL_uint32_t WasmEdge_ValueGetI32(in[_stack++])
#define VAL_int32_t WasmEdge_ValueGetI32(in[_stack++])
#define VAL_uint64_t WasmEdge_ValueGetI64(in[_stack++])
#define VAL_int64_t WasmEdge_ValueGetI64(in[_stack++])
#define VAR_ASSIGN(T, V) T V = CAT(VAL_##T)
// RET_* : wrap a C return value into a WasmValue
#define RET_uint32_t(return_code) hook::WasmValue::i32((uint32_t)(return_code))
#define RET_int32_t(return_code) hook::WasmValue::i32((uint32_t)(return_code))
#define RET_uint64_t(return_code) hook::WasmValue::i64((uint64_t)(return_code))
#define RET_int64_t(return_code) hook::WasmValue::i64((uint64_t)(return_code))
#define RET_uint32_t(return_code) WasmEdge_ValueGenI32(return_code)
#define RET_int32_t(return_code) WasmEdge_ValueGenI32(return_code)
#define RET_uint64_t(return_code) WasmEdge_ValueGenI64(return_code)
#define RET_int64_t(return_code) WasmEdge_ValueGenI64(return_code)
#define RET_ASSIGN(T, return_code) CAT2(RET_, T(return_code))
// TYP_* : map C type to WasmValue::Kind (used by WasmEdgeEngine.cpp registry)
#define TYP_uint32_t hook::WasmValue::Kind::I32
#define TYP_int32_t hook::WasmValue::Kind::I32
#define TYP_uint64_t hook::WasmValue::Kind::I64
#define TYP_int64_t hook::WasmValue::Kind::I64
#define TYP_uint32_t WasmEdge_ValType_I32
#define TYP_int32_t WasmEdge_ValType_I32
#define TYP_uint64_t WasmEdge_ValType_I64
#define TYP_int64_t WasmEdge_ValType_I64
#define WASM_VAL_TYPE(T, b) CAT2(TYP_, T)
#define UNSIGNED_TYPE(T) std::make_unsigned_t<T>
// DECLARE_HOOK_FUNCTION: forward-declares the impl function and the engine
// wrapper
#define DECLARE_HOOK_FUNCTION(R, F, ...) \
std::variant<UNSIGNED_TYPE(R), hook_api::hook_return_code> F( \
hook::HookContext& hookCtx, \
hook::GuestMemory& mem __VA_OPT__(COMMA __VA_ARGS__)); \
extern hook::HostCallStatus WasmFunction##F( \
void* userData, \
hook::GuestMemory& mem, \
hook::WasmValue const* in, \
size_t inLen, \
hook::WasmValue* out, \
size_t outLen);
WasmEdge_CallingFrameContext const& frameCtx __VA_OPT__( \
COMMA __VA_ARGS__)); \
extern WasmEdge_Result WasmFunction##F( \
void* data_ptr, \
const WasmEdge_CallingFrameContext* frameCtx, \
const WasmEdge_Value* in, \
WasmEdge_Value* out); \
extern WasmEdge_ValType WasmFunctionParams##F[]; \
extern WasmEdge_ValType WasmFunctionResult##F[]; \
extern WasmEdge_FunctionTypeContext* WasmFunctionType##F; \
extern WasmEdge_String WasmFunctionName##F;
// DEFINE_HOOK_FUNCTION: defines the engine-agnostic wrapper + the impl function
// body
#define DEFINE_HOOK_FUNCTION(R, F, ...) \
hook::HostCallStatus hook_api::WasmFunction##F( \
WasmEdge_Result hook_api::WasmFunction##F( \
void* data_ptr, \
hook::GuestMemory& mem, \
hook::WasmValue const* in, \
size_t /*inLen*/, \
hook::WasmValue* out, \
size_t /*outLen*/) \
const WasmEdge_CallingFrameContext* frameCtx, \
const WasmEdge_Value* in, \
WasmEdge_Value* out) \
{ \
__VA_OPT__(int _stack = 0;) \
__VA_OPT__(FOR_VARS(VAR_ASSIGN, 2, __VA_ARGS__);) \
hook::HookContext* hookCtx = \
reinterpret_cast<hook::HookContext*>(data_ptr); \
auto const& return_code = hook_api::F( \
*hookCtx, mem __VA_OPT__(COMMA STRIP_TYPES(__VA_ARGS__))); \
*hookCtx, \
*const_cast<WasmEdge_CallingFrameContext*>(frameCtx) \
__VA_OPT__(COMMA STRIP_TYPES(__VA_ARGS__))); \
if (std::holds_alternative<hook_api::hook_return_code>(return_code) && \
(std::get<hook_api::hook_return_code>(return_code) == \
RC_ROLLBACK || \
std::get<hook_api::hook_return_code>(return_code) == RC_ACCEPT)) \
return hook::HostCallStatus::Terminate; \
return WasmEdge_Result_Terminate; \
out[0] = RET_ASSIGN( \
R, \
std::holds_alternative<UNSIGNED_TYPE(R)>(return_code) \
? std::get<UNSIGNED_TYPE(R)>(return_code) \
: R(std::get<hook_api::hook_return_code>(return_code))); \
return hook::HostCallStatus::Success; \
return WasmEdge_Result_Success; \
}; \
WasmEdge_ValType hook_api::WasmFunctionParams##F[] = { \
__VA_OPT__(FOR_VARS(WASM_VAL_TYPE, 0, __VA_ARGS__))}; \
WasmEdge_ValType hook_api::WasmFunctionResult##F[1] = { \
WASM_VAL_TYPE(R, dummy)}; \
WasmEdge_FunctionTypeContext* hook_api::WasmFunctionType##F = \
WasmEdge_FunctionTypeCreate( \
WasmFunctionParams##F, \
VA_NARGS(NULL __VA_OPT__(, __VA_ARGS__)), \
WasmFunctionResult##F, \
1); \
WasmEdge_String hook_api::WasmFunctionName##F = \
WasmEdge_StringCreateByCString(#F); \
std::variant<UNSIGNED_TYPE(R), hook_api::hook_return_code> hook_api::F( \
hook::HookContext& hookCtx, \
hook::GuestMemory& mem __VA_OPT__(COMMA __VA_ARGS__))
WasmEdge_CallingFrameContext const& frameCtx __VA_OPT__( \
COMMA __VA_ARGS__))
#define HOOK_SETUP() \
using enum hook_api::hook_return_code; \
try \
{ \
[[maybe_unused]] ApplyContext& applyCtx = hookCtx.applyCtx; \
[[maybe_unused]] auto& view = applyCtx.view(); \
[[maybe_unused]] auto j = applyCtx.app.journal("View"); \
[[maybe_unused]] unsigned char* memory = mem.base; \
[[maybe_unused]] const uint64_t memory_length = mem.size; \
[[maybe_unused]] auto& api = hookCtx.api(); \
if (!memory || !memory_length) \
#define HOOK_SETUP() \
using enum hook_api::hook_return_code; \
try \
{ \
[[maybe_unused]] ApplyContext& applyCtx = hookCtx.applyCtx; \
[[maybe_unused]] auto& view = applyCtx.view(); \
[[maybe_unused]] auto j = applyCtx.app.journal("View"); \
[[maybe_unused]] WasmEdge_MemoryInstanceContext* memoryCtx = \
WasmEdge_CallingFrameGetMemoryInstance(&frameCtx, 0); \
[[maybe_unused]] unsigned char* memory = \
WasmEdge_MemoryInstanceGetPointer(memoryCtx, 0, 0); \
[[maybe_unused]] const uint64_t memory_length = \
WasmEdge_MemoryInstanceGetPageSize(memoryCtx) * \
WasmEdge_kPageSize; \
[[maybe_unused]] auto& api = hookCtx.api(); \
if (!memoryCtx || !memory || !memory_length) \
return INTERNAL_ERROR;
#define HOOK_TEARDOWN() \
@@ -185,10 +197,11 @@
<< " bytes past end of wasm memory"; \
return OUT_OF_BOUNDS; \
} \
if (!mem.write( \
if (!WasmEdge_ResultOK(WasmEdge_MemoryInstanceSetData( \
memoryCtx, \
reinterpret_cast<const uint8_t*>(host_src_ptr), \
guest_dst_ptr, \
reinterpret_cast<uint8_t const*>(host_src_ptr), \
static_cast<uint64_t>(bytes_to_write))) \
bytes_to_write))) \
return INTERNAL_ERROR; \
bytes_written += bytes_to_write; \
}

View File

@@ -1,114 +0,0 @@
#ifndef RIPPLE_HOOK_WASMTYPES_H_INCLUDED
#define RIPPLE_HOOK_WASMTYPES_H_INCLUDED
#include <xrpl/basics/base_uint.h>
#include <cstdint>
#include <cstring>
#include <vector>
namespace hook {
struct GuestMemory
{
uint8_t* base;
uint64_t size;
inline bool
inBounds(uint64_t guestPtr, uint64_t len) const
{
if (len > 0 && guestPtr > (UINT64_MAX - len + 1))
return false;
return (guestPtr + len) <= size;
}
inline bool
write(uint64_t guestPtr, void const* src, uint64_t len)
{
if (!inBounds(guestPtr, len))
return false;
std::memcpy(base + guestPtr, src, len);
return true;
}
inline bool
read(uint64_t guestPtr, void* dst, uint64_t len) const
{
if (!inBounds(guestPtr, len))
return false;
std::memcpy(dst, base + guestPtr, len);
return true;
}
};
struct WasmValue
{
enum Kind : uint8_t
{
I32,
I64
} kind;
union
{
uint32_t u32;
uint64_t u64;
};
static inline WasmValue
i32(uint32_t v)
{
WasmValue w;
w.kind = I32;
w.u32 = v;
return w;
}
static inline WasmValue
i64(uint64_t v)
{
WasmValue w;
w.kind = I64;
w.u64 = v;
return w;
}
inline uint32_t
asI32() const
{
return u32;
}
inline uint64_t
asI64() const
{
return u64;
}
};
enum class HostCallStatus
{
Success,
Terminate,
Trap,
};
using HostFunctionFn = HostCallStatus (*)(
void* userData,
GuestMemory& mem,
WasmValue const* in,
size_t inLen,
WasmValue* out,
size_t outLen);
struct HostFunctionDecl
{
char const* name;
HostFunctionFn fn;
std::vector<WasmValue::Kind> params;
WasmValue::Kind result;
ripple::uint256 const* featureGate;
};
} // namespace hook
#endif // RIPPLE_HOOK_WASMTYPES_H_INCLUDED

View File

@@ -34,7 +34,6 @@
// If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h.
XRPL_FEATURE(WasmtimeEngine, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (HookMap, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FIX (GuardDepth32, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(NamedHooks, Supported::yes, VoteBehavior::DefaultNo)

View File

@@ -15202,9 +15202,8 @@ public:
using namespace test::jtx;
static FeatureBitset const all{supported_amendments()};
static std::array<FeatureBitset, 9> const feats{
static std::array<FeatureBitset, 8> const feats{
all,
all - featureWasmtimeEngine,
all - fixXahauV2,
all - fixXahauV1 - fixXahauV2,
all - fixXahauV1 - fixXahauV2 - fixNSDelete,
@@ -15475,8 +15474,7 @@ SETHOOK_TEST(3, false)
SETHOOK_TEST(4, false)
SETHOOK_TEST(5, false)
SETHOOK_TEST(6, false)
SETHOOK_TEST(7, false)
SETHOOK_TEST(8, true)
SETHOOK_TEST(7, true)
BEAST_DEFINE_TESTSUITE_PRIO(SetHook0, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook1, app, ripple, 2);
@@ -15486,7 +15484,6 @@ BEAST_DEFINE_TESTSUITE_PRIO(SetHook4, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook5, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook6, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook7, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook8, app, ripple, 2);
} // namespace test
} // namespace ripple
#undef M

View File

@@ -97,6 +97,7 @@ struct StubHookContext
std::map<uint32_t, uint32_t> guard_map{};
StubHookResult result = {};
std::optional<ripple::STObject> emitFailure = std::nullopt;
const hook::HookExecutor* module = 0;
};
// Overload that takes external stateMap to avoid dangling reference

View File

@@ -145,7 +145,8 @@ makeStubHookContext(
.executeAgainAsWeak = result.executeAgainAsWeak,
.provisionalMeta = result.provisionalMeta,
},
.emitFailure = stubHookContext.emitFailure};
.emitFailure = stubHookContext.emitFailure,
.module = nullptr};
}
// Original function - WARNING: stateMap reference may become dangling

View File

@@ -17,6 +17,7 @@
#include <queue>
#include <utility>
#include <vector>
#include <wasmedge/wasmedge.h>
namespace hook {
struct HookContext;
@@ -178,6 +179,8 @@ struct HookResult
foreignStateGrantCache; // add found grants here to avoid rechecking
};
class HookExecutor;
struct SlotEntry
{
std::shared_ptr<const ripple::STObject> storage;
@@ -214,6 +217,7 @@ struct HookContext
emitFailure; // if this is a callback from a failed
// emitted txn then this optional becomes
// populated with the SLE
const HookExecutor* module = 0;
// Lazy-initialized HookAPI member
mutable std::unique_ptr<HookAPI> api_;
@@ -268,9 +272,231 @@ gatherHookParameters(
std::map<std::vector<uint8_t>, std::vector<uint8_t>>& parameters,
beast::Journal const& j_);
// RH TODO: call destruct for these on rippled shutdown
#define ADD_HOOK_FUNCTION(F, ctx) \
{ \
WasmEdge_FunctionInstanceContext* hf = \
WasmEdge_FunctionInstanceCreate( \
hook_api::WasmFunctionType##F, \
hook_api::WasmFunction##F, \
(void*)(&ctx), \
0); \
WasmEdge_ModuleInstanceAddFunction( \
importObj, hook_api::WasmFunctionName##F, hf); \
}
#define HR_ACC() hookResult.account << "-" << hookResult.otxnAccount
#define HC_ACC() hookCtx.result.account << "-" << hookCtx.result.otxnAccount
// create these once at boot and keep them
static WasmEdge_String exportName = WasmEdge_StringCreateByCString("env");
static WasmEdge_String tableName = WasmEdge_StringCreateByCString("table");
static auto* tableType = WasmEdge_TableTypeCreate(
WasmEdge_RefType_FuncRef,
{.HasMax = true, .Shared = false, .Min = 10, .Max = 20});
static auto* memType = WasmEdge_MemoryTypeCreate(
{.HasMax = true, .Shared = false, .Min = 1, .Max = 1});
static WasmEdge_String memName = WasmEdge_StringCreateByCString("memory");
static WasmEdge_String cbakFunctionName =
WasmEdge_StringCreateByCString("cbak");
static WasmEdge_String hookFunctionName =
WasmEdge_StringCreateByCString("hook");
// see: lib/system/allocator.cpp
#define WasmEdge_kPageSize 65536ULL
/**
* HookExecutor is effectively a two-part function:
* The first part sets up the Hook Api inside the wasm import, ready for use
* (this is done during object construction.)
* The second part is actually executing webassembly instructions
* this is done during execteWasm function.
* The instance is single use.
*/
class HookExecutor
{
private:
bool spent = false; // a HookExecutor can only be used once
public:
HookContext& hookCtx;
WasmEdge_ModuleInstanceContext* importObj;
class WasmEdgeVM
{
public:
WasmEdge_ConfigureContext* conf = NULL;
WasmEdge_VMContext* ctx = NULL;
WasmEdgeVM()
{
conf = WasmEdge_ConfigureCreate();
if (!conf)
return;
WasmEdge_ConfigureStatisticsSetInstructionCounting(conf, true);
ctx = WasmEdge_VMCreate(conf, NULL);
}
bool
sane()
{
return ctx && conf;
}
~WasmEdgeVM()
{
if (conf)
WasmEdge_ConfigureDelete(conf);
if (ctx)
WasmEdge_VMDelete(ctx);
}
};
// if an error occured return a string prefixed with `prefix` followed by
// the error description
static std::optional<std::string>
getWasmError(std::string prefix, WasmEdge_Result& res)
{
if (WasmEdge_ResultOK(res))
return {};
const char* msg = WasmEdge_ResultGetMessage(res);
return prefix + ": " + (msg ? msg : "unknown error");
}
/**
* Validate that a web assembly blob can be loaded by wasmedge
*/
static std::optional<std::string>
validateWasm(const void* wasm, size_t len)
{
WasmEdgeVM vm;
if (!vm.sane())
return "Could not create WASMEDGE instance";
WasmEdge_Result res = WasmEdge_VMLoadWasmFromBuffer(
vm.ctx, reinterpret_cast<const uint8_t*>(wasm), len);
if (auto err = getWasmError("VMLoadWasmFromBuffer failed", res); err)
return *err;
res = WasmEdge_VMValidate(vm.ctx);
if (auto err = getWasmError("VMValidate failed", res); err)
return *err;
return {};
}
/**
* Execute web assembly byte code against the constructed Hook Context
* Once execution has occured the exector is spent and cannot be used again
* and should be destructed Information about the execution is populated
* into hookCtx
*/
void
executeWasm(
const void* wasm,
size_t len,
bool callback,
uint32_t wasmParam,
beast::Journal const& j)
{
// HookExecutor can only execute once
XRPL_ASSERT(
!spent,
"HookExecutor::executeWasm : HookExecutor can only execute once");
spent = true;
JLOG(j.trace()) << "HookInfo[" << HC_ACC()
<< "]: creating wasm instance";
WasmEdge_LogOff();
WasmEdgeVM vm;
if (!vm.sane())
{
JLOG(j.warn()) << "HookError[" << HC_ACC()
<< "]: Could not create WASMEDGE instance.";
hookCtx.result.exitType = hook_api::ExitType::WASM_ERROR;
return;
}
WasmEdge_Result res =
WasmEdge_VMRegisterModuleFromImport(vm.ctx, this->importObj);
if (auto err = getWasmError("Import phase failed", res); err)
{
hookCtx.result.exitType = hook_api::ExitType::WASM_ERROR;
JLOG(j.trace()) << "HookError[" << HC_ACC() << "]: " << *err;
return;
}
WasmEdge_Value params[1] = {WasmEdge_ValueGenI32((int64_t)wasmParam)};
WasmEdge_Value returns[1];
res = WasmEdge_VMRunWasmFromBuffer(
vm.ctx,
reinterpret_cast<const uint8_t*>(wasm),
len,
callback ? cbakFunctionName : hookFunctionName,
params,
1,
returns,
1);
if (auto err = getWasmError("WASM VM error", res); err)
{
JLOG(j.warn()) << "HookError[" << HC_ACC() << "]: " << *err;
hookCtx.result.exitType = hook_api::ExitType::WASM_ERROR;
return;
}
auto* statsCtx = WasmEdge_VMGetStatisticsContext(vm.ctx);
hookCtx.result.instructionCount =
WasmEdge_StatisticsGetInstrCount(statsCtx);
// RH NOTE: stack unwind will clean up WasmEdgeVM
}
HookExecutor(HookContext& ctx)
: hookCtx(ctx), importObj(WasmEdge_ModuleInstanceCreate(exportName))
{
ctx.module = this;
WasmEdge_LogSetDebugLevel();
#pragma push_macro("HOOK_API_DEFINITION")
#undef HOOK_API_DEFINITION
#define HOOK_WRAP_PARAMS(...) __VA_ARGS__
#define HOOK_API_DEFINITION(RETURN_TYPE, FUNCTION_NAME, PARAMS_TUPLE, ...) \
ADD_HOOK_FUNCTION(FUNCTION_NAME, ctx);
#include <xrpl/hook/hook_api.macro>
#undef HOOK_API_DEFINITION
#undef HOOK_WRAP_PARAMS
#pragma pop_macro("HOOK_API_DEFINITION")
WasmEdge_TableInstanceContext* hostTable =
WasmEdge_TableInstanceCreate(tableType);
WasmEdge_ModuleInstanceAddTable(importObj, tableName, hostTable);
WasmEdge_MemoryInstanceContext* hostMem =
WasmEdge_MemoryInstanceCreate(memType);
WasmEdge_ModuleInstanceAddMemory(importObj, memName, hostMem);
}
~HookExecutor()
{
WasmEdge_ModuleInstanceDelete(importObj);
};
};
} // namespace hook
#endif

View File

@@ -1,336 +0,0 @@
#include <xrpld/app/hook/detail/WasmEdgeEngine.h>
#include <xrpld/app/hook/applyHook.h>
#include <xrpl/protocol/Feature.h>
#include <memory>
#include <optional>
#include <string>
#include <type_traits>
#include <vector>
#include <wasmedge/wasmedge.h>
using namespace ripple;
namespace hook {
namespace {
// ── Type-level helpers ──────────────────────────────────────────────────────
template <typename T>
constexpr WasmValue::Kind
kindOf()
{
if constexpr (
std::is_same_v<T, uint64_t> || std::is_same_v<T, int64_t>)
return WasmValue::Kind::I64;
return WasmValue::Kind::I32;
}
template <typename... Ts>
std::vector<WasmValue::Kind>
buildKinds()
{
return {kindOf<Ts>()...};
}
// ── WasmEdge VM RAII ────────────────────────────────────────────────────────
struct WasmEdgeVM
{
WasmEdge_ConfigureContext* conf = nullptr;
WasmEdge_VMContext* ctx = nullptr;
WasmEdgeVM()
{
conf = WasmEdge_ConfigureCreate();
if (!conf)
return;
WasmEdge_ConfigureStatisticsSetInstructionCounting(conf, true);
ctx = WasmEdge_VMCreate(conf, nullptr);
}
bool
sane() const
{
return ctx && conf;
}
~WasmEdgeVM()
{
if (ctx)
WasmEdge_VMDelete(ctx);
if (conf)
WasmEdge_ConfigureDelete(conf);
}
};
static std::optional<std::string>
getWasmError(std::string const& prefix, WasmEdge_Result res)
{
if (WasmEdge_ResultOK(res))
return {};
const char* msg = WasmEdge_ResultGetMessage(res);
return prefix + ": " + (msg ? msg : "unknown error");
}
// ── Bridge: single WasmEdge callback → HostFunctionFn ──────────────────────
struct BridgeData
{
HostFunctionDecl const* decl;
HookContext* ctx;
};
static WasmEdge_Result
bridgeFn(
void* data_ptr,
WasmEdge_CallingFrameContext const* frameCtx,
WasmEdge_Value const* in,
WasmEdge_Value* out)
{
auto* bridge = static_cast<BridgeData*>(data_ptr);
auto* memCtx = WasmEdge_CallingFrameGetMemoryInstance(frameCtx, 0);
GuestMemory mem{
WasmEdge_MemoryInstanceGetPointer(memCtx, 0, 0),
WasmEdge_MemoryInstanceGetPageSize(memCtx) * 65536ULL};
size_t const paramCount = bridge->decl->params.size();
std::vector<WasmValue> inVals(paramCount);
for (size_t i = 0; i < paramCount; ++i)
{
if (bridge->decl->params[i] == WasmValue::Kind::I32)
inVals[i] = WasmValue::i32((uint32_t)WasmEdge_ValueGetI32(in[i]));
else
inVals[i] = WasmValue::i64((uint64_t)WasmEdge_ValueGetI64(in[i]));
}
WasmValue outVal;
auto status = bridge->decl->fn(
bridge->ctx, mem, inVals.data(), paramCount, &outVal, 1);
if (status == HostCallStatus::Terminate || status == HostCallStatus::Trap)
return WasmEdge_Result_Terminate;
if (bridge->decl->result == WasmValue::Kind::I32)
out[0] = WasmEdge_ValueGenI32((int32_t)outVal.asI32());
else
out[0] = WasmEdge_ValueGenI64((int64_t)outVal.asI64());
return WasmEdge_Result_Success;
}
// ── WasmEdgeEngineImpl ──────────────────────────────────────────────────────
class WasmEdgeEngineImpl : public IWasmEngine
{
public:
std::optional<std::string>
validate(void const* wasm, size_t len) override
{
WasmEdgeVM vm;
if (!vm.sane())
return "Could not create WASMEDGE instance";
WasmEdge_Result res = WasmEdge_VMLoadWasmFromBuffer(
vm.ctx, reinterpret_cast<uint8_t const*>(wasm), len);
if (auto err = getWasmError("VMLoadWasmFromBuffer failed", res))
return *err;
res = WasmEdge_VMValidate(vm.ctx);
if (auto err = getWasmError("VMValidate failed", res))
return *err;
return {};
}
ExecutionResult
execute(
void const* wasm,
size_t len,
bool isCallback,
uint32_t wasmParam,
HookContext& ctx,
std::vector<HostFunctionDecl> const& imports,
ripple::Rules const& rules,
beast::Journal const& j) override
{
static WasmEdge_String exportName =
WasmEdge_StringCreateByCString("env");
static WasmEdge_String tableName =
WasmEdge_StringCreateByCString("table");
static auto* tableType = WasmEdge_TableTypeCreate(
WasmEdge_RefType_FuncRef,
{.HasMax = true, .Shared = false, .Min = 10, .Max = 20});
static auto* memType = WasmEdge_MemoryTypeCreate(
{.HasMax = true, .Shared = false, .Min = 1, .Max = 1});
static WasmEdge_String memName =
WasmEdge_StringCreateByCString("memory");
static WasmEdge_String cbakFunctionName =
WasmEdge_StringCreateByCString("cbak");
static WasmEdge_String hookFunctionName =
WasmEdge_StringCreateByCString("hook");
// Bridge data must not reallocate during registration
std::vector<BridgeData> bridgeData;
bridgeData.reserve(imports.size());
auto* importObj = WasmEdge_ModuleInstanceCreate(exportName);
for (auto const& decl : imports)
{
// featureGate: if non-null and not zero, check rules
if (decl.featureGate && !(*decl.featureGate).isZero() &&
!rules.enabled(*decl.featureGate))
continue;
bridgeData.push_back({&decl, &ctx});
std::vector<WasmEdge_ValType> paramTypes;
paramTypes.reserve(decl.params.size());
for (auto k : decl.params)
paramTypes.push_back(
k == WasmValue::Kind::I32 ? WasmEdge_ValType_I32
: WasmEdge_ValType_I64);
WasmEdge_ValType resultType =
decl.result == WasmValue::Kind::I32 ? WasmEdge_ValType_I32
: WasmEdge_ValType_I64;
auto* fnType = WasmEdge_FunctionTypeCreate(
paramTypes.data(), paramTypes.size(), &resultType, 1);
auto* fn = WasmEdge_FunctionInstanceCreate(
fnType, bridgeFn, &bridgeData.back(), 0);
WasmEdge_FunctionTypeDelete(fnType);
auto nameStr = WasmEdge_StringCreateByCString(decl.name);
WasmEdge_ModuleInstanceAddFunction(importObj, nameStr, fn);
WasmEdge_StringDelete(nameStr);
}
WasmEdge_ModuleInstanceAddTable(
importObj, tableName, WasmEdge_TableInstanceCreate(tableType));
WasmEdge_ModuleInstanceAddMemory(
importObj, memName, WasmEdge_MemoryInstanceCreate(memType));
JLOG(j.trace()) << "HookInfo[" << ctx.result.account << "-"
<< ctx.result.otxnAccount
<< "]: creating wasm instance";
WasmEdge_LogOff();
WasmEdgeVM vm;
if (!vm.sane())
{
WasmEdge_ModuleInstanceDelete(importObj);
JLOG(j.warn()) << "HookError[" << ctx.result.account << "-"
<< ctx.result.otxnAccount
<< "]: Could not create WASMEDGE instance.";
return {false, 0, "Could not create WASMEDGE instance"};
}
WasmEdge_Result res =
WasmEdge_VMRegisterModuleFromImport(vm.ctx, importObj);
if (auto err = getWasmError("Import phase failed", res))
{
JLOG(j.trace()) << "HookError[" << ctx.result.account << "-"
<< ctx.result.otxnAccount << "]: " << *err;
WasmEdge_ModuleInstanceDelete(importObj);
return {false, 0, *err};
}
WasmEdge_Value params[1] = {
WasmEdge_ValueGenI32((int64_t)wasmParam)};
WasmEdge_Value returns[1];
res = WasmEdge_VMRunWasmFromBuffer(
vm.ctx,
reinterpret_cast<uint8_t const*>(wasm),
len,
isCallback ? cbakFunctionName : hookFunctionName,
params,
1,
returns,
1);
ExecutionResult result;
if (auto err = getWasmError("WASM VM error", res))
{
result.ok = false;
result.instructionCount = 0;
result.error = *err;
}
else
{
result.ok = true;
auto* statsCtx = WasmEdge_VMGetStatisticsContext(vm.ctx);
result.instructionCount =
WasmEdge_StatisticsGetInstrCount(statsCtx);
}
WasmEdge_ModuleInstanceDelete(importObj);
return result;
}
};
} // anonymous namespace
// ── hookHostFunctionDecls registry ─────────────────────────────────────────
// Expand hook_api.macro with custom HOOK_API_DEFINITION to build the list.
// Each entry stores the WasmFunction##F pointer and parameter kinds.
// For featureGate: uint256{} means "always available" (stored as nullptr),
// named features are stored as their address.
#pragma push_macro("HOOK_API_DEFINITION")
#pragma push_macro("HOOK_WRAP_PARAMS")
#undef HOOK_API_DEFINITION
#undef HOOK_WRAP_PARAMS
// Helper to convert feature gate argument: uint256{} → nullptr,
// named feature variable → address of that variable.
// We use an inline helper function template to do this.
namespace {
template <typename T>
static inline uint256 const*
featureGatePtr(T const& gate)
{
// If gate is zero (default uint256{}), return nullptr (no gate).
if (gate.isZero())
return nullptr;
return &gate;
}
} // namespace
#define HOOK_WRAP_PARAMS(...) __VA_ARGS__
#define HOOK_API_DEFINITION(R, F, P, GATE) \
{ \
#F, \
&hook_api::WasmFunction##F, \
buildKinds<HOOK_WRAP_PARAMS P>(), \
kindOf<R>(), \
featureGatePtr(GATE), \
},
std::vector<HostFunctionDecl> const&
hookHostFunctionDecls()
{
static std::vector<HostFunctionDecl> const decls = {
#include <xrpl/hook/hook_api.macro>
};
return decls;
}
#undef HOOK_API_DEFINITION
#undef HOOK_WRAP_PARAMS
#pragma pop_macro("HOOK_WRAP_PARAMS")
#pragma pop_macro("HOOK_API_DEFINITION")
// ── Factory ─────────────────────────────────────────────────────────────────
std::unique_ptr<IWasmEngine>
makeWasmEdgeEngine()
{
return std::make_unique<WasmEdgeEngineImpl>();
}
} // namespace hook

View File

@@ -1,14 +0,0 @@
#ifndef RIPPLE_APP_HOOK_DETAIL_WASMEDGEENGINE_H_INCLUDED
#define RIPPLE_APP_HOOK_DETAIL_WASMEDGEENGINE_H_INCLUDED
#include <xrpld/app/hook/detail/WasmEngine.h>
#include <memory>
namespace hook {
std::unique_ptr<IWasmEngine>
makeWasmEdgeEngine();
} // namespace hook
#endif // RIPPLE_APP_HOOK_DETAIL_WASMEDGEENGINE_H_INCLUDED

View File

@@ -1,51 +0,0 @@
#ifndef RIPPLE_APP_HOOK_DETAIL_WASMENGINE_H_INCLUDED
#define RIPPLE_APP_HOOK_DETAIL_WASMENGINE_H_INCLUDED
#include <xrpl/hook/WasmTypes.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/Rules.h>
#include <optional>
#include <string>
#include <vector>
namespace hook {
/// Initial fuel budget per Hook execution (consensus-fixed; changing
/// this value requires a separate Amendment).
constexpr uint64_t kWasmtimeInitialFuel = 10'000'000'000ULL;
struct HookContext;
struct ExecutionResult
{
bool ok;
uint64_t instructionCount;
std::optional<std::string> error;
};
class IWasmEngine
{
public:
virtual ~IWasmEngine() = default;
virtual std::optional<std::string>
validate(void const* wasm, size_t len) = 0;
virtual ExecutionResult
execute(
void const* wasm,
size_t len,
bool isCallback,
uint32_t wasmParam,
HookContext& ctx,
std::vector<HostFunctionDecl> const& imports,
ripple::Rules const& rules,
beast::Journal const& j) = 0;
};
std::vector<HostFunctionDecl> const&
hookHostFunctionDecls();
} // namespace hook
#endif // RIPPLE_APP_HOOK_DETAIL_WASMENGINE_H_INCLUDED

View File

@@ -1,819 +0,0 @@
// WasmtimeEngine.cpp — Wasmtime backend for the IWasmEngine abstraction.
//
// WHY THIS FILE IS LARGER THAN WasmEdgeEngine.cpp
// ─────────────────────────────────────────────────
// WasmEdge provides WasmEdge_CallingFrameGetMemoryInstance(frame, index),
// which retrieves a module's linear memory by index regardless of whether it
// is exported. Wasmtime has no equivalent: the only way to access memory
// from inside a host-function callback is wasmtime_caller_export_get(), which
// only works for *exported* memories.
//
// Hook WASM modules compiled by wasmcc define their own linear memory
// (WebAssembly section 5) but do not export it. Without an export the memory
// pointer is unavailable inside host callbacks, causing every Hook API call
// that touches guest memory to see mem.base == nullptr and return
// INTERNAL_ERROR, ultimately crashing the execution.
//
// The ensureMemoryExported() function (≈200 lines) works around this by
// scanning the binary before instantiation and injecting a "memory" export
// entry into section 7 when the module owns but does not export its memory.
// The patched binary is semantically identical to the original.
//
// A potential alternative — storing the wasmtime_memory_t handle directly in
// BridgeData — requires knowing at setup time which memory the module will
// actually use (its own vs. an import from "env"), which itself requires
// binary inspection. The export-injection approach is therefore the simplest
// correct solution.
#include <xrpld/app/hook/detail/WasmtimeEngine.h>
#include <xrpld/app/hook/detail/WasmEngine.h>
#include <xrpld/app/hook/applyHook.h>
#include <xrpl/protocol/Feature.h>
#include <cstring>
#include <memory>
#include <optional>
#include <string>
#include <type_traits>
#include <vector>
// wasmtime.h must come LAST to avoid int128_t pollution
#include <wasmtime.h>
using namespace ripple;
namespace hook {
namespace {
// ── Type-level helpers ──────────────────────────────────────────────────────
template <typename T>
constexpr WasmValue::Kind
kindOf()
{
if constexpr (
std::is_same_v<T, uint64_t> || std::is_same_v<T, int64_t>)
return WasmValue::Kind::I64;
return WasmValue::Kind::I32;
}
template <typename... Ts>
std::vector<WasmValue::Kind>
buildKinds()
{
return {kindOf<Ts>()...};
}
// ── Error helpers ────────────────────────────────────────────────────────────
static std::optional<std::string>
wasmtimeError(wasmtime_error_t* err)
{
if (!err)
return {};
wasm_byte_vec_t msg;
wasmtime_error_message(err, &msg);
std::string s(msg.data, msg.size);
wasm_byte_vec_delete(&msg);
wasmtime_error_delete(err);
return s;
}
static std::optional<std::string>
wasmtimeTrap(wasm_trap_t* trap)
{
if (!trap)
return {};
wasm_byte_vec_t msg;
wasm_trap_message(trap, &msg);
std::string s(msg.data, msg.size);
wasm_byte_vec_delete(&msg);
wasm_trap_delete(trap);
return s;
}
// ── Process-global engine (thread-safe; created once) ────────────────────────
static wasm_engine_t*
getGlobalEngine()
{
static wasm_engine_t* gEngine = []() -> wasm_engine_t* {
wasm_config_t* cfg = wasm_config_new();
if (!cfg)
return nullptr;
// Consensus-fixed configuration never change without an Amendment
wasmtime_config_consume_fuel_set(cfg, true);
wasmtime_config_wasm_simd_set(cfg, true);
wasmtime_config_wasm_relaxed_simd_set(cfg, false);
wasmtime_config_wasm_reference_types_set(cfg, true);
wasmtime_config_wasm_bulk_memory_set(cfg, true);
wasmtime_config_wasm_multi_value_set(cfg, false);
wasmtime_config_cranelift_nan_canonicalization_set(cfg, true);
// wasm_threads is behind a feature flag; only set if available.
// The compile-time feature guard in config.h controls whether
// wasmtime_config_wasm_threads_set exists.
#ifdef WASMTIME_FEATURE_THREADS
wasmtime_config_wasm_threads_set(cfg, false);
#endif
// wasm_engine_new_with_config takes ownership of cfg
return wasm_engine_new_with_config(cfg);
}();
return gEngine;
}
// ── WASM binary normalisation ─────────────────────────────────────────────────
//
// Hook WASM modules typically have their own memory (WebAssembly Section 5)
// but do NOT export it. Wasmtime's wasmtime_caller_export_get() only works
// for *exported* memories, so without an export the memory pointer is
// unavailable inside host callbacks.
//
// This function inspects the binary and, when the module owns a memory but
// does not already export it as "memory", injects a new export entry for
// memory index 0. The resulting binary is semantically identical to the
// original but now exposes its memory to the host.
//
// Modules that already export "memory" (or that import memory from "env") are
// returned unchanged.
// LEB128 helpers (unsigned, forward-only)
static uint32_t
readUleb128(uint8_t const* p, size_t& pos, size_t limit)
{
uint32_t result = 0;
uint32_t shift = 0;
while (pos < limit)
{
uint8_t b = p[pos++];
result |= (uint32_t)(b & 0x7F) << shift;
if (!(b & 0x80))
break;
shift += 7;
}
return result;
}
// Encode a value as unsigned LEB128 (appends to out)
static void
writeUleb128(std::vector<uint8_t>& out, uint32_t val)
{
do
{
uint8_t b = val & 0x7F;
val >>= 7;
if (val)
b |= 0x80;
out.push_back(b);
} while (val);
}
// Ensure the WASM binary exports its first memory as "memory".
// Returns the original bytes unchanged if no patching is needed,
// otherwise returns a patched copy.
static std::vector<uint8_t>
ensureMemoryExported(void const* wasm, size_t len)
{
uint8_t const* p = reinterpret_cast<uint8_t const*>(wasm);
// Validate magic + version.
// NOTE: the magic bytes are 0x00 0x61 0x73 0x6D 0x01 0x00 0x00 0x00
// Written as a string literal, "\x00asm…" is ambiguous because \x hex
// escapes are greedy: "\x00a" == "\x0a" (newline). Use a byte array.
static constexpr uint8_t kWasmMagic[8] = {
0x00, 0x61, 0x73, 0x6D, // \0asm
0x01, 0x00, 0x00, 0x00 // version 1
};
if (len < 8 || std::memcmp(p, kWasmMagic, 8) != 0)
return {p, p + len}; // not a valid WASM binary, return as-is
bool hasOwnMemory = false;
bool exportsMemory = false;
// offset to memory section (for the insert point) and export section
size_t exportSectionOffset = 0; // byte offset of existing export section
size_t exportSectionPayloadOff = 0; // offset of payload start
size_t exportSectionPayloadLen = 0; // payload length
size_t pos = 8;
while (pos + 1 < len)
{
uint8_t sectionId = p[pos++];
size_t secLenPos = pos;
uint32_t secLen = readUleb128(p, pos, len);
size_t payloadStart = pos;
if (sectionId == 5) // memory section
{
// At least one memory entry means the module owns a memory
size_t tmp = pos;
uint32_t count = readUleb128(p, tmp, len);
if (count > 0)
hasOwnMemory = true;
}
else if (sectionId == 7) // export section
{
exportSectionOffset = secLenPos - 1; // start of section id byte
exportSectionPayloadOff = payloadStart;
exportSectionPayloadLen = secLen;
size_t tmp = pos;
uint32_t count = readUleb128(p, tmp, len);
for (uint32_t i = 0; i < count; ++i)
{
uint32_t nameLen = readUleb128(p, tmp, len);
if (tmp + nameLen > len)
break;
bool isMemoryName = (nameLen == 6 &&
std::memcmp(p + tmp, "memory", 6) == 0);
tmp += nameLen;
if (tmp >= len)
break;
uint8_t kind = p[tmp++]; // export kind byte
readUleb128(p, tmp, len); // export index
if (isMemoryName && kind == 0x02) // kind 2 == memory
{
exportsMemory = true;
break;
}
}
}
pos = payloadStart + secLen;
}
// No patching needed if:
// - module does not own a memory (it may import one; caller_export_get works)
// - module already exports its memory
if (!hasOwnMemory || exportsMemory)
return {p, p + len};
// Build a new memory export entry: \x06 m e m o r y \x02 \x00
// (name_len=6, "memory", kind=2 (memory), index=0)
std::vector<uint8_t> newExportEntry;
writeUleb128(newExportEntry, 6); // name length
for (char c : std::string("memory"))
newExportEntry.push_back((uint8_t)c);
newExportEntry.push_back(0x02); // export kind: memory
writeUleb128(newExportEntry, 0); // memory index 0
if (exportSectionOffset == 0)
{
// No export section exists at all create one from scratch.
// New section: id=7, payload = count(1) + entry
std::vector<uint8_t> newSection;
newSection.push_back(0x07); // export section id
std::vector<uint8_t> payload;
writeUleb128(payload, 1); // 1 export
payload.insert(payload.end(), newExportEntry.begin(), newExportEntry.end());
writeUleb128(newSection, (uint32_t)payload.size());
newSection.insert(newSection.end(), payload.begin(), payload.end());
// Insert the new section before the code section (id=10) or at end.
// For correctness we append after the last known section.
std::vector<uint8_t> result(p, p + len);
result.insert(result.end(), newSection.begin(), newSection.end());
return result;
}
else
{
// Patch the existing export section: increment count, append entry.
// Read existing export count (as a LEB128)
size_t countPos = exportSectionPayloadOff;
uint32_t existingCount = readUleb128(p, countPos, len);
uint32_t newCount = existingCount + 1;
std::vector<uint8_t> newCountBytes;
writeUleb128(newCountBytes, newCount);
// New payload = new count + existing entries (skip old count bytes) + new entry
std::vector<uint8_t> newPayload;
newPayload.insert(newPayload.end(), newCountBytes.begin(), newCountBytes.end());
// Existing entries start at countPos, end at exportSectionPayloadOff + exportSectionPayloadLen
size_t entriesStart = countPos;
size_t entriesEnd = exportSectionPayloadOff + exportSectionPayloadLen;
if (entriesEnd > len)
entriesEnd = len;
newPayload.insert(newPayload.end(), p + entriesStart, p + entriesEnd);
newPayload.insert(newPayload.end(), newExportEntry.begin(), newExportEntry.end());
// Encode new section length as LEB128
std::vector<uint8_t> newSecLen;
writeUleb128(newSecLen, (uint32_t)newPayload.size());
// Reconstruct the full binary:
// bytes before export section + id(0x07) + new_len + new_payload + bytes after
std::vector<uint8_t> result;
result.reserve(len + newExportEntry.size() + 4);
// Part 1: everything before the export section
result.insert(result.end(), p, p + exportSectionOffset);
// Part 2: section id
result.push_back(0x07);
// Part 3: new length
result.insert(result.end(), newSecLen.begin(), newSecLen.end());
// Part 4: new payload
result.insert(result.end(), newPayload.begin(), newPayload.end());
// Part 5: everything after the export section
size_t afterExport = exportSectionPayloadOff + exportSectionPayloadLen;
if (afterExport < len)
result.insert(result.end(), p + afterExport, p + len);
return result;
}
}
// ── Per-execution state shared between the host callback and caller ──────────
struct ExecState
{
bool terminated = false;
};
struct BridgeData
{
HostFunctionDecl const* decl;
HookContext* ctx;
ExecState* state;
};
// ── wasm_functype_t builder helper ───────────────────────────────────────────
static wasm_functype_t*
buildFuncType(
std::vector<WasmValue::Kind> const& params,
WasmValue::Kind result)
{
wasm_valtype_vec_t paramVec;
wasm_valtype_vec_t resultVec;
if (params.empty())
{
wasm_valtype_vec_new_empty(&paramVec);
}
else
{
std::vector<wasm_valtype_t*> ptrs;
ptrs.reserve(params.size());
for (auto k : params)
ptrs.push_back(
wasm_valtype_new(k == WasmValue::Kind::I32 ? WASM_I32 : WASM_I64));
wasm_valtype_vec_new(&paramVec, ptrs.size(), ptrs.data());
}
{
wasm_valtype_t* rs[1] = {
wasm_valtype_new(result == WasmValue::Kind::I32 ? WASM_I32 : WASM_I64)};
wasm_valtype_vec_new(&resultVec, 1, rs);
}
return wasm_functype_new(&paramVec, &resultVec);
}
// ── Bridge callback ──────────────────────────────────────────────────────────
static wasm_trap_t*
bridgeFn(
void* envPtr,
wasmtime_caller_t* caller,
wasmtime_val_t const* args,
size_t nargs,
wasmtime_val_t* results,
size_t nresults)
{
auto* bridge = static_cast<BridgeData*>(envPtr);
// The module's memory is always exported as "memory" after
// ensureMemoryExported() normalises the binary. For modules that import
// memory from "env" (rare) the imported memory is also accessible via
// the same export path once the linker resolves it.
wasmtime_extern_t memExtern;
GuestMemory mem{nullptr, 0};
if (wasmtime_caller_export_get(
caller, "memory", 6 /* strlen("memory") */, &memExtern))
{
if (memExtern.kind == WASMTIME_EXTERN_MEMORY)
{
wasmtime_context_t* ctx = wasmtime_caller_context(caller);
mem.base = wasmtime_memory_data(ctx, &memExtern.of.memory);
mem.size = wasmtime_memory_data_size(ctx, &memExtern.of.memory);
}
}
// Convert incoming wasmtime_val_t args to WasmValue
size_t const paramCount = bridge->decl->params.size();
std::vector<WasmValue> inVals(paramCount);
for (size_t i = 0; i < paramCount && i < nargs; ++i)
{
if (bridge->decl->params[i] == WasmValue::Kind::I32)
inVals[i] = WasmValue::i32((uint32_t)args[i].of.i32);
else
inVals[i] = WasmValue::i64((uint64_t)args[i].of.i64);
}
WasmValue outVal;
auto status =
bridge->decl->fn(bridge->ctx, mem, inVals.data(), paramCount, &outVal, 1);
if (status == HostCallStatus::Terminate || status == HostCallStatus::Trap)
{
// Signal termination to the outer caller
bridge->state->terminated = true;
// Return a trap to unwind the WASM stack
return wasmtime_trap_new("hook terminated", 15);
}
// Write result
if (nresults > 0)
{
if (bridge->decl->result == WasmValue::Kind::I32)
{
results[0].kind = WASMTIME_I32;
results[0].of.i32 = (int32_t)outVal.asI32();
}
else
{
results[0].kind = WASMTIME_I64;
results[0].of.i64 = (int64_t)outVal.asI64();
}
}
return nullptr; // success
}
// ── WasmtimeEngineImpl ───────────────────────────────────────────────────────
class WasmtimeEngineImpl : public IWasmEngine
{
public:
std::optional<std::string>
validate(void const* wasm, size_t len) override
{
wasm_engine_t* engine = getGlobalEngine();
if (!engine)
return "Could not create Wasmtime engine";
// Normalise the binary so validation sees the same form as execution.
auto patched = ensureMemoryExported(wasm, len);
wasmtime_module_t* mod = nullptr;
wasmtime_error_t* err = wasmtime_module_new(
engine,
patched.data(),
patched.size(),
&mod);
if (auto msg = wasmtimeError(err))
return "Wasmtime validate failed: " + *msg;
wasmtime_module_delete(mod);
return {};
}
ExecutionResult
execute(
void const* wasm,
size_t len,
bool isCallback,
uint32_t wasmParam,
HookContext& ctx,
std::vector<HostFunctionDecl> const& imports,
ripple::Rules const& rules,
beast::Journal const& j) override
{
wasm_engine_t* engine = getGlobalEngine();
if (!engine)
return {false, 0, "Could not obtain Wasmtime engine"};
// Ensure the module's memory is exported so that bridgeFn can access it
// via wasmtime_caller_export_get("memory"). Hook modules often define
// their own memory section without a corresponding export entry.
auto patched = ensureMemoryExported(wasm, len);
// ── Compile module ─────────────────────────────────────────────────
wasmtime_module_t* mod = nullptr;
{
wasmtime_error_t* err = wasmtime_module_new(
engine,
patched.data(),
patched.size(),
&mod);
if (auto msg = wasmtimeError(err))
return {false, 0, "Wasmtime compile failed: " + *msg};
}
// ── Create store + context ─────────────────────────────────────────
wasmtime_store_t* store = wasmtime_store_new(engine, nullptr, nullptr);
if (!store)
{
wasmtime_module_delete(mod);
return {false, 0, "Could not create Wasmtime store"};
}
wasmtime_context_t* storeCtx = wasmtime_store_context(store);
// Set initial fuel (consensus-fixed)
{
wasmtime_error_t* err =
wasmtime_context_set_fuel(storeCtx, kWasmtimeInitialFuel);
if (auto msg = wasmtimeError(err))
{
wasmtime_store_delete(store);
wasmtime_module_delete(mod);
return {false, 0, "Could not set fuel: " + *msg};
}
}
// ── Per-execution termination state ───────────────────────────────
ExecState execState;
// ── Build bridge data (must stay stable in memory) ─────────────────
std::vector<BridgeData> bridgeData;
bridgeData.reserve(imports.size());
// ── Create linker ──────────────────────────────────────────────────
wasmtime_linker_t* linker = wasmtime_linker_new(engine);
static const char kEnvModule[] = "env";
static const size_t kEnvModuleLen = 3; // strlen("env")
for (auto const& decl : imports)
{
// Feature gate: skip if this API is behind a disabled Amendment
if (decl.featureGate && !(*decl.featureGate).isZero() &&
!rules.enabled(*decl.featureGate))
continue;
bridgeData.push_back({&decl, &ctx, &execState});
wasm_functype_t* ft = buildFuncType(decl.params, decl.result);
// NOTE: wasmtime_linker_define_func takes a raw `data` pointer
// but does NOT call the finalizer per-call (only when the linker
// is deleted or shadowed). Since bridgeData is alive for the
// duration of the execution, this is safe.
wasmtime_error_t* err = wasmtime_linker_define_func(
linker,
kEnvModule,
kEnvModuleLen,
decl.name,
std::strlen(decl.name),
ft,
bridgeFn,
&bridgeData.back(),
nullptr);
wasm_functype_delete(ft);
if (auto msg = wasmtimeError(err))
{
wasmtime_linker_delete(linker);
wasmtime_store_delete(store);
wasmtime_module_delete(mod);
return {false, 0, "Linker define_func failed: " + *msg};
}
}
// ── Define the "table" import (funcref, min=10, max=20) ────────────
{
wasm_limits_t tableLimits = {10, 20};
wasm_tabletype_t* tt =
wasm_tabletype_new(wasm_valtype_new(WASM_FUNCREF), &tableLimits);
wasmtime_table_t tbl;
wasmtime_val_t initVal;
initVal.kind = WASMTIME_FUNCREF;
wasmtime_funcref_set_null(&initVal.of.funcref);
wasmtime_error_t* err =
wasmtime_table_new(storeCtx, tt, &initVal, &tbl);
wasm_tabletype_delete(tt);
if (auto msg = wasmtimeError(err))
{
wasmtime_linker_delete(linker);
wasmtime_store_delete(store);
wasmtime_module_delete(mod);
return {false, 0, "Table creation failed: " + *msg};
}
wasmtime_extern_t ext;
ext.kind = WASMTIME_EXTERN_TABLE;
ext.of.table = tbl;
err = wasmtime_linker_define(
linker,
storeCtx,
kEnvModule,
kEnvModuleLen,
"table",
5,
&ext);
if (auto msg = wasmtimeError(err))
{
wasmtime_linker_delete(linker);
wasmtime_store_delete(store);
wasmtime_module_delete(mod);
return {false, 0, "Linker define table failed: " + *msg};
}
}
// ── Define the "memory" import (min=1, max=1 pages) ───────────────
// This is provided for modules that import their memory from "env".
// Modules with own memory sections (Section 5) ignore this definition
// and use their own memory instead (which ensureMemoryExported() will
// have made accessible via a "memory" export).
{
wasm_memorytype_t* mt;
{
wasmtime_error_t* err = wasmtime_memorytype_new(
1, // min pages
true, // max_present
1, // max pages
false, // is_64
false, // shared
0, // page_size_log2 (0 = default 64KiB)
&mt);
if (auto msg = wasmtimeError(err))
{
wasmtime_linker_delete(linker);
wasmtime_store_delete(store);
wasmtime_module_delete(mod);
return {false, 0, "Memory type creation failed: " + *msg};
}
}
wasmtime_memory_t mem;
wasmtime_error_t* err = wasmtime_memory_new(storeCtx, mt, &mem);
wasm_memorytype_delete(mt);
if (auto msg = wasmtimeError(err))
{
wasmtime_linker_delete(linker);
wasmtime_store_delete(store);
wasmtime_module_delete(mod);
return {false, 0, "Memory creation failed: " + *msg};
}
wasmtime_extern_t ext;
ext.kind = WASMTIME_EXTERN_MEMORY;
ext.of.memory = mem;
err = wasmtime_linker_define(
linker,
storeCtx,
kEnvModule,
kEnvModuleLen,
"memory",
6,
&ext);
if (auto msg = wasmtimeError(err))
{
wasmtime_linker_delete(linker);
wasmtime_store_delete(store);
wasmtime_module_delete(mod);
return {false, 0, "Linker define memory failed: " + *msg};
}
}
JLOG(j.trace()) << "HookInfo[" << ctx.result.account << "-"
<< ctx.result.otxnAccount
<< "]: creating wasmtime instance";
// ── Instantiate ───────────────────────────────────────────────────
wasmtime_instance_t instance;
{
wasm_trap_t* trap = nullptr;
wasmtime_error_t* err = wasmtime_linker_instantiate(
linker, storeCtx, mod, &instance, &trap);
if (auto msg = wasmtimeError(err))
{
wasmtime_linker_delete(linker);
wasmtime_store_delete(store);
wasmtime_module_delete(mod);
return {false, 0, "Instantiation error: " + *msg};
}
if (trap)
{
auto msg = wasmtimeTrap(trap);
wasmtime_linker_delete(linker);
wasmtime_store_delete(store);
wasmtime_module_delete(mod);
return {
false,
0,
"Instantiation trap: " + (msg ? *msg : "unknown")};
}
}
// ── Look up "hook" or "cbak" export ───────────────────────────────
const char* funcName = isCallback ? "cbak" : "hook";
size_t funcNameLen = isCallback ? 4 : 4;
wasmtime_extern_t funcExtern;
if (!wasmtime_instance_export_get(
storeCtx,
&instance,
funcName,
funcNameLen,
&funcExtern) ||
funcExtern.kind != WASMTIME_EXTERN_FUNC)
{
wasmtime_linker_delete(linker);
wasmtime_store_delete(store);
wasmtime_module_delete(mod);
return {
false,
0,
std::string("WASM export '") + funcName + "' not found"};
}
// ── Call hook/cbak with wasmParam ──────────────────────────────────
wasmtime_val_t callArgs[1];
callArgs[0].kind = WASMTIME_I32;
callArgs[0].of.i32 = (int32_t)wasmParam;
wasmtime_val_t callResults[1];
wasm_trap_t* callTrap = nullptr;
wasmtime_error_t* callErr = wasmtime_func_call(
storeCtx,
&funcExtern.of.func,
callArgs,
1,
callResults,
1,
&callTrap);
// Capture fuel consumed before cleaning up
uint64_t fuelRemaining = 0;
wasmtime_context_get_fuel(storeCtx, &fuelRemaining);
uint64_t instructionCount = 0;
if (kWasmtimeInitialFuel >= fuelRemaining)
instructionCount = kWasmtimeInitialFuel - fuelRemaining;
wasmtime_linker_delete(linker);
wasmtime_store_delete(store);
wasmtime_module_delete(mod);
// ── Interpret result ───────────────────────────────────────────────
//
// When a host callback returns a wasm_trap_t* to signal accept() or
// rollback(), Wasmtime propagates that as a wasmtime_error_t (with the
// trap embedded as the cause) rather than via the wasm_trap_t** output.
// We therefore check execState.terminated FIRST — before inspecting
// callErr or callTrap — so that clean hook terminations (accept/rollback)
// are always reported as ok=true regardless of which output pointer
// Wasmtime chose to use.
if (execState.terminated)
{
// Hook called accept() or rollback(): clean termination.
// Free any error/trap resources Wasmtime may have allocated.
if (callErr)
wasmtime_error_delete(callErr);
if (callTrap)
wasm_trap_delete(callTrap);
return {true, instructionCount, {}};
}
// Programmer error (wrong arg types, mismatched store, etc.)
if (callErr)
{
auto msg = wasmtimeError(callErr);
return {false, instructionCount, "WASM call error: " + (msg ? *msg : "")};
}
if (callTrap)
{
// Check for out-of-fuel trap
wasmtime_trap_code_t code = 0;
if (wasmtime_trap_code(callTrap, &code) &&
code == WASMTIME_TRAP_CODE_OUT_OF_FUEL)
{
wasm_trap_delete(callTrap);
return {false, instructionCount, "WASM out of fuel"};
}
auto msg = wasmtimeTrap(callTrap);
return {
false,
instructionCount,
"WASM trap: " + (msg ? *msg : "unknown trap")};
}
return {true, instructionCount, {}};
}
};
} // anonymous namespace
// ── Factory ──────────────────────────────────────────────────────────────────
std::unique_ptr<IWasmEngine>
makeWasmtimeEngine()
{
return std::make_unique<WasmtimeEngineImpl>();
}
} // namespace hook

View File

@@ -1,14 +0,0 @@
#ifndef RIPPLE_APP_HOOK_DETAIL_WASMTIMEENGINE_H_INCLUDED
#define RIPPLE_APP_HOOK_DETAIL_WASMTIMEENGINE_H_INCLUDED
#include <xrpld/app/hook/detail/WasmEngine.h>
#include <memory>
namespace hook {
std::unique_ptr<IWasmEngine>
makeWasmtimeEngine();
} // namespace hook
#endif // RIPPLE_APP_HOOK_DETAIL_WASMTIMEENGINE_H_INCLUDED

View File

@@ -19,9 +19,7 @@
#include <string>
#include <utility>
#include <vector>
#include <xrpld/app/hook/detail/WasmEngine.h>
#include <xrpld/app/hook/detail/WasmEdgeEngine.h>
#include <xrpld/app/hook/detail/WasmtimeEngine.h>
#include <wasmedge/wasmedge.h>
using namespace ripple;
@@ -1084,25 +1082,10 @@ hook::apply(
auto const& j = applyCtx.app.journal("View");
auto engine = applyCtx.view().rules().enabled(featureWasmtimeEngine)
? makeWasmtimeEngine()
: makeWasmEdgeEngine();
auto const engineResult = engine->execute(
wasm.data(),
(size_t)wasm.size(),
isCallback,
wasmParam,
hookCtx,
hookHostFunctionDecls(),
applyCtx.view().rules(),
j);
hookCtx.result.instructionCount = engineResult.instructionCount;
if (!engineResult.ok)
{
hookCtx.result.exitType = hook_api::ExitType::WASM_ERROR;
JLOG(j.warn()) << "HookError[" << HC_ACC() << "]: "
<< engineResult.error.value_or("unknown");
}
HookExecutor executor{hookCtx};
executor.executeWasm(
wasm.data(), (size_t)wasm.size(), isCallback, wasmParam, j);
JLOG(j.trace()) << "HookInfo[" << HC_ACC() << "]: "
<< (hookCtx.result.exitType == hook_api::ExitType::ROLLBACK
@@ -1273,7 +1256,7 @@ DEFINE_HOOK_FUNCTION(
{
return state_foreign_set(
hookCtx,
mem,
frameCtx,
read_ptr,
read_len,
kread_ptr,
@@ -1651,7 +1634,7 @@ DEFINE_HOOK_FUNCTION(
{
return state_foreign(
hookCtx,
mem,
frameCtx,
write_ptr,
write_len,
kread_ptr,
@@ -3213,7 +3196,7 @@ DEFINE_HOOK_FUNCTION(
// proxy only no setup or teardown
auto ret = sto_emplace(
hookCtx,
mem,
frameCtx,
write_ptr,
write_len,
read_ptr,

View File

@@ -18,8 +18,6 @@
//==============================================================================
#include <xrpld/app/hook/applyHook.h>
#include <xrpld/app/hook/detail/WasmEdgeEngine.h>
#include <xrpld/app/hook/detail/WasmtimeEngine.h>
#include <xrpld/app/ledger/Ledger.h>
#include <xrpld/app/main/Application.h>
#include <xrpld/app/misc/AmendmentTable.h>
@@ -639,11 +637,8 @@ Change::activateXahauGenesis()
return;
}
auto wasmValidator = ctx_.view().rules().enabled(featureWasmtimeEngine)
? hook::makeWasmtimeEngine()
: hook::makeWasmEdgeEngine();
std::optional<std::string> result2 =
wasmValidator->validate(
hook::HookExecutor::validateWasm(
wasmBytes.data(), (size_t)wasmBytes.size());
if (result2)

View File

@@ -48,9 +48,7 @@
#include <utility>
#include <variant>
#include <vector>
#include <xrpld/app/hook/detail/WasmEngine.h>
#include <xrpld/app/hook/detail/WasmEdgeEngine.h>
#include <xrpld/app/hook/detail/WasmtimeEngine.h>
#include <wasmedge/wasmedge.h>
#define DEBUG_GUARD_CHECK 1
#define HS_ACC() \
@@ -594,11 +592,8 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
<< "]: Trying to wasm instantiate proposed hook "
<< "size = " << hook.size();
auto wasmValidator = ctx.rules.enabled(featureWasmtimeEngine)
? hook::makeWasmtimeEngine()
: hook::makeWasmEdgeEngine();
std::optional<std::string> result2 =
wasmValidator->validate(
hook::HookExecutor::validateWasm(
hook.data(), (size_t)hook.size());
if (result2)