Initial reorg.
This is largely the same as #305, but updated for the current tree.
This commit is contained in:
44
crates/api/Cargo.toml
Normal file
44
crates/api/Cargo.toml
Normal file
@@ -0,0 +1,44 @@
|
||||
[package]
|
||||
name = "wasmtime-api"
|
||||
authors = ["The Wasmtime Project Developers"]
|
||||
version = "0.1.0"
|
||||
description = "High-level API to expose the Wasmtime runtime"
|
||||
license = "Apache-2.0 WITH LLVM-exception"
|
||||
repository = "https://github.com/CraneStation/wasmtime"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
name = "wasmtime_api"
|
||||
crate-type = ["lib", "staticlib", "cdylib"]
|
||||
|
||||
[dependencies]
|
||||
cranelift-codegen = { version = "0.49", features = ["enable-serde"] }
|
||||
cranelift-native = { version = "0.49" }
|
||||
cranelift-entity = { version = "0.49", features = ["enable-serde"] }
|
||||
cranelift-wasm = { version = "0.49", features = ["enable-serde"] }
|
||||
cranelift-frontend = { version = "0.49" }
|
||||
wasmtime-runtime = { path="../runtime" }
|
||||
wasmtime-environ = { path="../environ" }
|
||||
wasmtime-jit = { path="../jit" }
|
||||
wasmparser = { version = "0.39.2", default-features = false }
|
||||
target-lexicon = { version = "0.9.0", default-features = false }
|
||||
anyhow = "1.0.19"
|
||||
thiserror = "1.0.4"
|
||||
region = "2.0.0"
|
||||
hashbrown = { version = "0.6.0", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = ["cranelift-codegen/std", "cranelift-wasm/std", "wasmtime-environ/std", "wasmparser/std"]
|
||||
core = ["hashbrown/nightly", "cranelift-codegen/core", "cranelift-wasm/core", "wasmtime-environ/core", "wasmparser/core"]
|
||||
|
||||
[dev-dependencies]
|
||||
# for wasmtime.rs
|
||||
wasi-common = { path = "../wasi-common" }
|
||||
docopt = "1.0.1"
|
||||
serde = { "version" = "1.0.94", features = ["derive"] }
|
||||
pretty_env_logger = "0.3.0"
|
||||
wasmtime-wast = { path="../wast" }
|
||||
wasmtime-wasi = { path="../wasi" }
|
||||
rayon = "1.1"
|
||||
file-per-thread-logger = "0.1.1"
|
||||
220
crates/api/LICENSE
Normal file
220
crates/api/LICENSE
Normal file
@@ -0,0 +1,220 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
--- LLVM Exceptions to the Apache 2.0 License ----
|
||||
|
||||
As an exception, if, as a result of your compiling your source code, portions
|
||||
of this Software are embedded into an Object form of such source code, you
|
||||
may redistribute such embedded portions in such Object form without complying
|
||||
with the conditions of Sections 4(a), 4(b) and 4(d) of the License.
|
||||
|
||||
In addition, if you combine or link compiled forms of this Software with
|
||||
software that is licensed under the GPLv2 ("Combined Software") and if a
|
||||
court of competent jurisdiction determines that the patent provision (Section
|
||||
3), the indemnity provision (Section 9) or other Section of the License
|
||||
conflicts with the conditions of the GPLv2, you may retroactively and
|
||||
prospectively choose to deem waived or otherwise exclude such Section(s) of
|
||||
the License, but only in their entirety and only with respect to the Combined
|
||||
Software.
|
||||
|
||||
3
crates/api/README.md
Normal file
3
crates/api/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Implementation of wasm-c-api in Rust
|
||||
|
||||
https://github.com/WebAssembly/wasm-c-api
|
||||
155
crates/api/c-examples/Makefile
Normal file
155
crates/api/c-examples/Makefile
Normal file
@@ -0,0 +1,155 @@
|
||||
###############################################################################
|
||||
# Configuration
|
||||
|
||||
# Inherited from wasm-c-api/Makefile to just run C examples
|
||||
|
||||
WASM_FLAGS = -DWASM_API_DEBUG # -DWASM_API_DEBUG_LOG
|
||||
C_FLAGS = ${WASM_FLAGS} -Wall -Werror -ggdb -O -fsanitize=address
|
||||
CC_FLAGS = -std=c++11 ${C_FLAGS}
|
||||
LD_FLAGS = -fsanitize-memory-track-origins -fsanitize-memory-use-after-dtor
|
||||
|
||||
C_COMP = clang
|
||||
|
||||
WASMTIME_API_MODE = debug
|
||||
|
||||
|
||||
# Base directories
|
||||
WASMTIME_API_DIR = ..
|
||||
WASM_DIR = wasm-c-api
|
||||
EXAMPLE_DIR = ${WASM_DIR}/example
|
||||
OUT_DIR = ${WASM_DIR}/out
|
||||
|
||||
# Example config
|
||||
EXAMPLE_OUT = ${OUT_DIR}/example
|
||||
EXAMPLES = \
|
||||
hello \
|
||||
callback \
|
||||
trap \
|
||||
start \
|
||||
reflect \
|
||||
global \
|
||||
table \
|
||||
memory \
|
||||
hostref \
|
||||
finalize \
|
||||
serialize \
|
||||
threads \
|
||||
# multi \
|
||||
|
||||
# Wasm config
|
||||
WASM_INCLUDE = ${WASM_DIR}/include
|
||||
WASM_SRC = ${WASM_DIR}/src
|
||||
WASM_OUT = ${OUT_DIR}
|
||||
WASM_C_LIBS = wasm-bin wasm-rust-api
|
||||
WASM_CC_LIBS = $(error unsupported C++)
|
||||
|
||||
|
||||
# Compiler config
|
||||
ifeq (${WASMTIME_API_MODE},release)
|
||||
CARGO_BUILD_FLAGS = --release
|
||||
else
|
||||
CARGO_BUILD_FLAGS =
|
||||
endif
|
||||
|
||||
ifeq (${C_COMP},clang)
|
||||
CC_COMP = clang++
|
||||
LD_GROUP_START =
|
||||
LD_GROUP_END =
|
||||
else ifeq (${C_COMP},gcc)
|
||||
CC_COMP = g++
|
||||
LD_GROUP_START = -Wl,--start-group
|
||||
LD_GROUP_END = -Wl,--end-group
|
||||
else
|
||||
$(error C_COMP set to unknown compiler, must be clang or gcc)
|
||||
endif
|
||||
|
||||
WASMTIME_BIN_DIR = ${WASMTIME_API_DIR}/../target/${WASMTIME_API_MODE}
|
||||
WASMTIME_C_LIB_DIR = ${WASMTIME_BIN_DIR}
|
||||
WASMTIME_C_LIBS = wasmtime_api
|
||||
WASMTIME_CC_LIBS = $(error unsupported c++)
|
||||
|
||||
WASMTIME_C_BINS = ${WASMTIME_C_LIBS:%=${WASMTIME_C_LIB_DIR}/lib%.a}
|
||||
|
||||
###############################################################################
|
||||
# Examples
|
||||
#
|
||||
# To build Wasm APIs and run all examples:
|
||||
# make all
|
||||
#
|
||||
# To run only C examples:
|
||||
# make c
|
||||
#
|
||||
# To run only C++ examples:
|
||||
# make cc
|
||||
#
|
||||
# To run individual C example (e.g. hello):
|
||||
# make run-hello-c
|
||||
#
|
||||
# To run individual C++ example (e.g. hello):
|
||||
# make run-hello-cc
|
||||
#
|
||||
|
||||
.PHONY: all cc c
|
||||
all: cc c
|
||||
cc: ${EXAMPLES:%=run-%-cc}
|
||||
c: ${EXAMPLES:%=run-%-c}
|
||||
|
||||
# Running a C / C++ example
|
||||
run-%-c: ${EXAMPLE_OUT}/%-c ${EXAMPLE_OUT}/%.wasm
|
||||
@echo ==== C ${@:run-%-c=%} ====; \
|
||||
cd ${EXAMPLE_OUT}; ./${@:run-%=%}
|
||||
@echo ==== Done ====
|
||||
|
||||
run-%-cc: ${EXAMPLE_OUT}/%-cc ${EXAMPLE_OUT}/%.wasm
|
||||
@echo ==== C++ ${@:run-%-cc=%} ====; \
|
||||
cd ${EXAMPLE_OUT}; ./${@:run-%=%}
|
||||
@echo ==== Done ====
|
||||
|
||||
# Compiling C / C++ example
|
||||
${EXAMPLE_OUT}/%-c.o: ${EXAMPLE_DIR}/%.c ${WASM_INCLUDE}/wasm.h
|
||||
mkdir -p ${EXAMPLE_OUT}
|
||||
${C_COMP} -c ${C_FLAGS} -I. -I${WASM_INCLUDE} $< -o $@
|
||||
|
||||
${EXAMPLE_OUT}/%-cc.o: ${EXAMPLE_DIR}/%.cc ${WASM_INCLUDE}/wasm.hh
|
||||
mkdir -p ${EXAMPLE_OUT}
|
||||
${CC_COMP} -c ${CC_FLAGS} -I. -I${WASM_INCLUDE} $< -o $@
|
||||
|
||||
# Linking C / C++ example
|
||||
.PRECIOUS: ${EXAMPLES:%=${EXAMPLE_OUT}/%-c}
|
||||
${EXAMPLE_OUT}/%-c: ${EXAMPLE_OUT}/%-c.o ${WASMTIME_C_BINS}
|
||||
${CC_COMP} ${CC_FLAGS} ${LD_FLAGS} $< -o $@ \
|
||||
${LD_GROUP_START} \
|
||||
${WASMTIME_C_BINS} \
|
||||
${LD_GROUP_END} \
|
||||
-ldl -pthread
|
||||
|
||||
# Installing Wasm binaries
|
||||
.PRECIOUS: ${EXAMPLES:%=${EXAMPLE_OUT}/%.wasm}
|
||||
${EXAMPLE_OUT}/%.wasm: ${EXAMPLE_DIR}/%.wasm
|
||||
cp $< $@
|
||||
|
||||
###############################################################################
|
||||
# Wasm C / C++ API
|
||||
#
|
||||
# To build both C / C++ APIs:
|
||||
# make wasm
|
||||
|
||||
.PHONY: wasm wasm-c wasm-cc
|
||||
wasm: wasm-c wasm-cc
|
||||
wasm-c: ${WASMTIME_C_BIN}
|
||||
wasm-cc: ${WASMTIME_CC_BIN}
|
||||
|
||||
${WASMTIME_C_BINS}: CARGO_RUN
|
||||
cd ${WASMTIME_API_DIR}; cargo build --lib ${CARGO_BUILD_FLAGS}
|
||||
|
||||
.PHONY: CARGO_RUN
|
||||
CARGO_RUN:
|
||||
|
||||
|
||||
###############################################################################
|
||||
# Clean-up
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf ${OUT_DIR}
|
||||
|
||||
1
crates/api/c-examples/wasm-c-api
Submodule
1
crates/api/c-examples/wasm-c-api
Submodule
Submodule crates/api/c-examples/wasm-c-api added at 8782d5b456
43
crates/api/examples/gcd.rs
Normal file
43
crates/api/examples/gcd.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
//! Example of instantiating of the WebAssembly module and
|
||||
//! invoking its exported function.
|
||||
|
||||
use anyhow::{format_err, Result};
|
||||
use std::fs::read;
|
||||
use wasmtime_api::*;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let wasm = read("examples/gcd.wasm")?;
|
||||
|
||||
// Instantiate engine and store.
|
||||
let engine = HostRef::new(Engine::default());
|
||||
let store = HostRef::new(Store::new(&engine));
|
||||
|
||||
// Load a module.
|
||||
let module = HostRef::new(Module::new(&store, &wasm)?);
|
||||
|
||||
// Find index of the `gcd` export.
|
||||
let gcd_index = module
|
||||
.borrow()
|
||||
.exports()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, export)| export.name().to_string() == "gcd")
|
||||
.unwrap()
|
||||
.0;
|
||||
|
||||
// Instantiate the module.
|
||||
let instance = HostRef::new(Instance::new(&store, &module, &[])?);
|
||||
|
||||
// Invoke `gcd` export
|
||||
let gcd = instance.borrow().exports()[gcd_index]
|
||||
.func()
|
||||
.expect("gcd")
|
||||
.clone();
|
||||
let result = gcd
|
||||
.borrow()
|
||||
.call(&[Val::from(6i32), Val::from(27i32)])
|
||||
.map_err(|e| format_err!("call error: {:?}", e))?;
|
||||
|
||||
println!("{:?}", result);
|
||||
Ok(())
|
||||
}
|
||||
BIN
crates/api/examples/gcd.wasm
Normal file
BIN
crates/api/examples/gcd.wasm
Normal file
Binary file not shown.
68
crates/api/examples/hello.rs
Normal file
68
crates/api/examples/hello.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
//! Translation of hello example
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
use alloc::rc::Rc;
|
||||
use anyhow::{ensure, format_err, Context as _, Result};
|
||||
use core::cell::Ref;
|
||||
use std::fs::read;
|
||||
use wasmtime_api::*;
|
||||
|
||||
struct HelloCallback;
|
||||
|
||||
impl Callable for HelloCallback {
|
||||
fn call(&self, _params: &[Val], _results: &mut [Val]) -> Result<(), HostRef<Trap>> {
|
||||
println!("Calling back...");
|
||||
println!("> Hello World!");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// Initialize.
|
||||
println!("Initializing...");
|
||||
let engine = HostRef::new(Engine::default());
|
||||
let store = HostRef::new(Store::new(&engine));
|
||||
|
||||
// Load binary.
|
||||
println!("Loading binary...");
|
||||
let binary = read("examples/hello.wasm")?;
|
||||
|
||||
// Compile.
|
||||
println!("Compiling module...");
|
||||
let module = HostRef::new(Module::new(&store, &binary).context("> Error compiling module!")?);
|
||||
|
||||
// Create external print functions.
|
||||
println!("Creating callback...");
|
||||
let hello_type = FuncType::new(Box::new([]), Box::new([]));
|
||||
let hello_func = HostRef::new(Func::new(&store, hello_type, Rc::new(HelloCallback)));
|
||||
|
||||
// Instantiate.
|
||||
println!("Instantiating module...");
|
||||
let imports = vec![hello_func.into()];
|
||||
let instance = HostRef::new(
|
||||
Instance::new(&store, &module, imports.as_slice())
|
||||
.context("> Error instantiating module!")?,
|
||||
);
|
||||
|
||||
// Extract export.
|
||||
println!("Extracting export...");
|
||||
let exports = Ref::map(instance.borrow(), |instance| instance.exports());
|
||||
ensure!(!exports.is_empty(), "> Error accessing exports!");
|
||||
let run_func = exports[0].func().context("> Error accessing exports!")?;
|
||||
|
||||
// Call.
|
||||
println!("Calling export...");
|
||||
run_func
|
||||
.borrow()
|
||||
.call(&[])
|
||||
.map_err(|e| format_err!("> Error calling function: {:?}", e))?;
|
||||
|
||||
// Shut down.
|
||||
println!("Shutting down...");
|
||||
drop(store);
|
||||
|
||||
// All done.
|
||||
println!("Done.");
|
||||
Ok(())
|
||||
}
|
||||
BIN
crates/api/examples/hello.wasm
Normal file
BIN
crates/api/examples/hello.wasm
Normal file
Binary file not shown.
4
crates/api/examples/hello.wat
Normal file
4
crates/api/examples/hello.wat
Normal file
@@ -0,0 +1,4 @@
|
||||
(module
|
||||
(func $hello (import "" "hello"))
|
||||
(func (export "run") (call $hello))
|
||||
)
|
||||
153
crates/api/examples/memory.rs
Normal file
153
crates/api/examples/memory.rs
Normal file
@@ -0,0 +1,153 @@
|
||||
//! Translation of the memory example
|
||||
|
||||
use anyhow::{bail, ensure, Context as _, Error};
|
||||
use core::cell::Ref;
|
||||
use std::fs::read;
|
||||
use wasmtime_api::*;
|
||||
|
||||
fn get_export_memory(exports: &[Extern], i: usize) -> Result<HostRef<Memory>, Error> {
|
||||
if exports.len() <= i {
|
||||
bail!("> Error accessing memory export {}!", i);
|
||||
}
|
||||
Ok(exports[i]
|
||||
.memory()
|
||||
.with_context(|| format!("> Error accessing memory export {}!", i))?
|
||||
.clone())
|
||||
}
|
||||
|
||||
fn get_export_func(exports: &[Extern], i: usize) -> Result<HostRef<Func>, Error> {
|
||||
if exports.len() <= i {
|
||||
bail!("> Error accessing function export {}!", i);
|
||||
}
|
||||
Ok(exports[i]
|
||||
.func()
|
||||
.with_context(|| format!("> Error accessing function export {}!", i))?
|
||||
.clone())
|
||||
}
|
||||
|
||||
macro_rules! check {
|
||||
($actual:expr, $expected:expr) => {
|
||||
if $actual != $expected {
|
||||
bail!("> Error on result, expected {}, got {}", $expected, $actual);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! check_ok {
|
||||
($func:expr, $($p:expr),*) => {
|
||||
if let Err(_) = $func.borrow().call(&[$($p.into()),*]) {
|
||||
bail!("> Error on result, expected return");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! check_trap {
|
||||
($func:expr, $($p:expr),*) => {
|
||||
if let Ok(_) = $func.borrow().call(&[$($p.into()),*]) {
|
||||
bail!("> Error on result, expected trap");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! call {
|
||||
($func:expr, $($p:expr),*) => {
|
||||
match $func.borrow().call(&[$($p.into()),*]) {
|
||||
Ok(result) => {
|
||||
let result: i32 = result[0].clone().into();
|
||||
result
|
||||
}
|
||||
Err(_) => { bail!("> Error on result, expected return"); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
// Initialize.
|
||||
println!("Initializing...");
|
||||
let engine = HostRef::new(Engine::default());
|
||||
let store = HostRef::new(Store::new(&engine));
|
||||
|
||||
// Load binary.
|
||||
println!("Loading binary...");
|
||||
let binary = read("examples/memory.wasm")?;
|
||||
|
||||
// Compile.
|
||||
println!("Compiling module...");
|
||||
let module = HostRef::new(Module::new(&store, &binary).context("> Error compiling module!")?);
|
||||
|
||||
// Instantiate.
|
||||
println!("Instantiating module...");
|
||||
let instance =
|
||||
HostRef::new(Instance::new(&store, &module, &[]).context("> Error instantiating module!")?);
|
||||
|
||||
// Extract export.
|
||||
println!("Extracting export...");
|
||||
let exports = Ref::map(instance.borrow(), |instance| instance.exports());
|
||||
ensure!(!exports.is_empty(), "> Error accessing exports!");
|
||||
let memory = get_export_memory(&exports, 0)?;
|
||||
let size_func = get_export_func(&exports, 1)?;
|
||||
let load_func = get_export_func(&exports, 2)?;
|
||||
let store_func = get_export_func(&exports, 3)?;
|
||||
|
||||
// Try cloning.
|
||||
check!(memory.clone().ptr_eq(&memory), true);
|
||||
|
||||
// Check initial memory.
|
||||
println!("Checking memory...");
|
||||
check!(memory.borrow().size(), 2u32);
|
||||
check!(memory.borrow().data_size(), 0x20000usize);
|
||||
check!(unsafe { memory.borrow().data()[0] }, 0);
|
||||
check!(unsafe { memory.borrow().data()[0x1000] }, 1);
|
||||
check!(unsafe { memory.borrow().data()[0x1003] }, 4);
|
||||
|
||||
check!(call!(size_func,), 2);
|
||||
check!(call!(load_func, 0), 0);
|
||||
check!(call!(load_func, 0x1000), 1);
|
||||
check!(call!(load_func, 0x1003), 4);
|
||||
check!(call!(load_func, 0x1ffff), 0);
|
||||
check_trap!(load_func, 0x20000);
|
||||
|
||||
// Mutate memory.
|
||||
println!("Mutating memory...");
|
||||
unsafe {
|
||||
memory.borrow_mut().data()[0x1003] = 5;
|
||||
}
|
||||
|
||||
check_ok!(store_func, 0x1002, 6);
|
||||
check_trap!(store_func, 0x20000, 0);
|
||||
|
||||
check!(unsafe { memory.borrow().data()[0x1002] }, 6);
|
||||
check!(unsafe { memory.borrow().data()[0x1003] }, 5);
|
||||
check!(call!(load_func, 0x1002), 6);
|
||||
check!(call!(load_func, 0x1003), 5);
|
||||
|
||||
// Grow memory.
|
||||
println!("Growing memory...");
|
||||
check!(memory.borrow_mut().grow(1), true);
|
||||
check!(memory.borrow().size(), 3u32);
|
||||
check!(memory.borrow().data_size(), 0x30000usize);
|
||||
|
||||
check!(call!(load_func, 0x20000), 0);
|
||||
check_ok!(store_func, 0x20000, 0);
|
||||
check_trap!(load_func, 0x30000);
|
||||
check_trap!(store_func, 0x30000, 0);
|
||||
|
||||
check!(memory.borrow_mut().grow(1), false);
|
||||
check!(memory.borrow_mut().grow(0), true);
|
||||
|
||||
// Create stand-alone memory.
|
||||
// TODO(wasm+): Once Wasm allows multiple memories, turn this into import.
|
||||
println!("Creating stand-alone memory...");
|
||||
let memorytype = MemoryType::new(Limits::new(5, 5));
|
||||
let mut memory2 = Memory::new(&store, memorytype);
|
||||
check!(memory2.size(), 5u32);
|
||||
check!(memory2.grow(1), false);
|
||||
check!(memory2.grow(0), true);
|
||||
|
||||
// Shut down.
|
||||
println!("Shutting down...");
|
||||
drop(store);
|
||||
|
||||
println!("Done.");
|
||||
Ok(())
|
||||
}
|
||||
BIN
crates/api/examples/memory.wasm
Normal file
BIN
crates/api/examples/memory.wasm
Normal file
Binary file not shown.
11
crates/api/examples/memory.wat
Normal file
11
crates/api/examples/memory.wat
Normal file
@@ -0,0 +1,11 @@
|
||||
(module
|
||||
(memory (export "memory") 2 3)
|
||||
|
||||
(func (export "size") (result i32) (memory.size))
|
||||
(func (export "load") (param i32) (result i32) (i32.load8_s (local.get 0)))
|
||||
(func (export "store") (param i32 i32)
|
||||
(i32.store8 (local.get 0) (local.get 1))
|
||||
)
|
||||
|
||||
(data (i32.const 0x1000) "\01\02\03\04")
|
||||
)
|
||||
81
crates/api/examples/multi.rs
Normal file
81
crates/api/examples/multi.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
//! Translation of multi example
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
use alloc::rc::Rc;
|
||||
use anyhow::{ensure, format_err, Context as _, Result};
|
||||
use core::cell::Ref;
|
||||
use std::fs::read;
|
||||
use wasmtime_api::*;
|
||||
|
||||
struct Callback;
|
||||
|
||||
impl Callable for Callback {
|
||||
fn call(&self, args: &[Val], results: &mut [Val]) -> Result<(), HostRef<Trap>> {
|
||||
println!("Calling back...");
|
||||
println!("> {} {}", args[0].i32(), args[1].i64());
|
||||
|
||||
results[0] = Val::I64(args[1].i64() + 1);
|
||||
results[1] = Val::I32(args[0].i32() + 1);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// Initialize.
|
||||
println!("Initializing...");
|
||||
let engine = HostRef::new(Engine::default());
|
||||
let store = HostRef::new(Store::new(&engine));
|
||||
|
||||
// Load binary.
|
||||
println!("Loading binary...");
|
||||
let binary = read("examples/multi.wasm")?;
|
||||
|
||||
// Compile.
|
||||
println!("Compiling module...");
|
||||
let module = HostRef::new(Module::new(&store, &binary).context("Error compiling module!")?);
|
||||
|
||||
// Create external print functions.
|
||||
println!("Creating callback...");
|
||||
let callback_type = FuncType::new(
|
||||
Box::new([ValType::I32, ValType::I64]),
|
||||
Box::new([ValType::I64, ValType::I32]),
|
||||
);
|
||||
let callback_func = HostRef::new(Func::new(&store, callback_type, Rc::new(Callback)));
|
||||
|
||||
// Instantiate.
|
||||
println!("Instantiating module...");
|
||||
let imports = vec![callback_func.into()];
|
||||
let instance = HostRef::new(
|
||||
Instance::new(&store, &module, imports.as_slice())
|
||||
.context("Error instantiating module!")?,
|
||||
);
|
||||
|
||||
// Extract export.
|
||||
println!("Extracting export...");
|
||||
let exports = Ref::map(instance.borrow(), |instance| instance.exports());
|
||||
ensure!(!exports.is_empty(), "Error accessing exports!");
|
||||
let run_func = exports[0].func().context("Error accessing exports!")?;
|
||||
|
||||
// Call.
|
||||
println!("Calling export...");
|
||||
let args = vec![Val::I32(1), Val::I64(3)];
|
||||
let results = run_func
|
||||
.borrow()
|
||||
.call(&args)
|
||||
.map_err(|e| format_err!("> Error calling function: {:?}", e))?;
|
||||
|
||||
println!("Printing result...");
|
||||
println!("> {} {}", results[0].i64(), results[1].i32());
|
||||
|
||||
debug_assert!(results[0].i64() == 4);
|
||||
debug_assert!(results[1].i32() == 2);
|
||||
|
||||
// Shut down.
|
||||
println!("Shutting down...");
|
||||
drop(store);
|
||||
|
||||
// All done.
|
||||
println!("Done.");
|
||||
Ok(())
|
||||
}
|
||||
BIN
crates/api/examples/multi.wasm
Normal file
BIN
crates/api/examples/multi.wasm
Normal file
Binary file not shown.
7
crates/api/examples/multi.wat
Normal file
7
crates/api/examples/multi.wat
Normal file
@@ -0,0 +1,7 @@
|
||||
(module
|
||||
(func $f (import "" "f") (param i32 i64) (result i64 i32))
|
||||
|
||||
(func $g (export "g") (param i32 i64) (result i64 i32)
|
||||
(call $f (local.get 0) (local.get 1))
|
||||
)
|
||||
)
|
||||
155
crates/api/src/callable.rs
Normal file
155
crates/api/src/callable.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
use crate::r#ref::HostRef;
|
||||
use crate::runtime::Store;
|
||||
use crate::trap::Trap;
|
||||
use crate::types::FuncType;
|
||||
use crate::values::Val;
|
||||
use alloc::rc::Rc;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::trampoline::generate_func_export;
|
||||
use cranelift_codegen::ir;
|
||||
use wasmtime_jit::InstanceHandle;
|
||||
use wasmtime_runtime::Export;
|
||||
|
||||
pub trait Callable {
|
||||
fn call(&self, params: &[Val], results: &mut [Val]) -> Result<(), HostRef<Trap>>;
|
||||
}
|
||||
|
||||
pub(crate) trait WrappedCallable {
|
||||
fn call(&self, params: &[Val], results: &mut [Val]) -> Result<(), HostRef<Trap>>;
|
||||
fn signature(&self) -> &ir::Signature {
|
||||
match self.wasmtime_export() {
|
||||
Export::Function { signature, .. } => signature,
|
||||
_ => panic!("unexpected export type in Callable"),
|
||||
}
|
||||
}
|
||||
fn wasmtime_handle(&self) -> &InstanceHandle;
|
||||
fn wasmtime_export(&self) -> &Export;
|
||||
}
|
||||
|
||||
pub(crate) struct WasmtimeFn {
|
||||
store: HostRef<Store>,
|
||||
instance: InstanceHandle,
|
||||
export: Export,
|
||||
}
|
||||
|
||||
impl WasmtimeFn {
|
||||
pub fn new(store: &HostRef<Store>, instance: InstanceHandle, export: Export) -> WasmtimeFn {
|
||||
WasmtimeFn {
|
||||
store: store.clone(),
|
||||
instance,
|
||||
export,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WrappedCallable for WasmtimeFn {
|
||||
fn call(&self, params: &[Val], results: &mut [Val]) -> Result<(), HostRef<Trap>> {
|
||||
use core::cmp::max;
|
||||
use core::{mem, ptr};
|
||||
|
||||
let (vmctx, body, signature) = match self.wasmtime_export() {
|
||||
Export::Function {
|
||||
vmctx,
|
||||
address,
|
||||
signature,
|
||||
} => (*vmctx, *address, signature.clone()),
|
||||
_ => panic!("unexpected export type in Callable"),
|
||||
};
|
||||
|
||||
let value_size = mem::size_of::<u64>();
|
||||
let mut values_vec: Vec<u64> = vec![0; max(params.len(), results.len())];
|
||||
|
||||
// Store the argument values into `values_vec`.
|
||||
for (index, arg) in params.iter().enumerate() {
|
||||
unsafe {
|
||||
let ptr = values_vec.as_mut_ptr().add(index);
|
||||
|
||||
match arg {
|
||||
Val::I32(x) => ptr::write(ptr as *mut i32, *x),
|
||||
Val::I64(x) => ptr::write(ptr as *mut i64, *x),
|
||||
Val::F32(x) => ptr::write(ptr as *mut u32, *x),
|
||||
Val::F64(x) => ptr::write(ptr as *mut u64, *x),
|
||||
_ => unimplemented!("WasmtimeFn arg"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the trampoline to call for this function.
|
||||
let exec_code_buf = self
|
||||
.store
|
||||
.borrow_mut()
|
||||
.context()
|
||||
.compiler()
|
||||
.get_published_trampoline(body, &signature, value_size)
|
||||
.map_err(|_| HostRef::new(Trap::fake()))?; //was ActionError::Setup)?;
|
||||
|
||||
// Call the trampoline.
|
||||
if let Err(message) = unsafe {
|
||||
wasmtime_runtime::wasmtime_call_trampoline(
|
||||
vmctx,
|
||||
exec_code_buf,
|
||||
values_vec.as_mut_ptr() as *mut u8,
|
||||
)
|
||||
} {
|
||||
return Err(HostRef::new(Trap::new(message)));
|
||||
}
|
||||
|
||||
// Load the return values out of `values_vec`.
|
||||
for (index, abi_param) in signature.returns.iter().enumerate() {
|
||||
unsafe {
|
||||
let ptr = values_vec.as_ptr().add(index);
|
||||
|
||||
results[index] = match abi_param.value_type {
|
||||
ir::types::I32 => Val::I32(ptr::read(ptr as *const i32)),
|
||||
ir::types::I64 => Val::I64(ptr::read(ptr as *const i64)),
|
||||
ir::types::F32 => Val::F32(ptr::read(ptr as *const u32)),
|
||||
ir::types::F64 => Val::F64(ptr::read(ptr as *const u64)),
|
||||
other => panic!("unsupported value type {:?}", other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
fn wasmtime_handle(&self) -> &InstanceHandle {
|
||||
&self.instance
|
||||
}
|
||||
fn wasmtime_export(&self) -> &Export {
|
||||
&self.export
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NativeCallable {
|
||||
callable: Rc<dyn Callable + 'static>,
|
||||
instance: InstanceHandle,
|
||||
export: Export,
|
||||
}
|
||||
|
||||
impl NativeCallable {
|
||||
pub(crate) fn new(
|
||||
callable: Rc<dyn Callable + 'static>,
|
||||
ft: &FuncType,
|
||||
store: &HostRef<Store>,
|
||||
) -> Self {
|
||||
let (instance, export) =
|
||||
generate_func_export(ft, &callable, store).expect("generated func");
|
||||
NativeCallable {
|
||||
callable,
|
||||
instance,
|
||||
export,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WrappedCallable for NativeCallable {
|
||||
fn call(&self, params: &[Val], results: &mut [Val]) -> Result<(), HostRef<Trap>> {
|
||||
self.callable.call(params, results)
|
||||
}
|
||||
fn wasmtime_handle(&self) -> &InstanceHandle {
|
||||
&self.instance
|
||||
}
|
||||
fn wasmtime_export(&self) -> &Export {
|
||||
&self.export
|
||||
}
|
||||
}
|
||||
68
crates/api/src/context.rs
Normal file
68
crates/api/src/context.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use alloc::rc::Rc;
|
||||
use core::cell::{RefCell, RefMut};
|
||||
use core::hash::{Hash, Hasher};
|
||||
|
||||
use wasmtime_jit::{CompilationStrategy, Compiler, Features};
|
||||
|
||||
use cranelift_codegen::settings;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Context {
|
||||
compiler: Rc<RefCell<Compiler>>,
|
||||
features: Features,
|
||||
debug_info: bool,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn new(compiler: Compiler, features: Features, debug_info: bool) -> Context {
|
||||
Context {
|
||||
compiler: Rc::new(RefCell::new(compiler)),
|
||||
features,
|
||||
debug_info,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create(
|
||||
flags: settings::Flags,
|
||||
features: Features,
|
||||
debug_info: bool,
|
||||
strategy: CompilationStrategy,
|
||||
) -> Context {
|
||||
Context::new(create_compiler(flags, strategy), features, debug_info)
|
||||
}
|
||||
|
||||
pub(crate) fn debug_info(&self) -> bool {
|
||||
self.debug_info
|
||||
}
|
||||
|
||||
pub(crate) fn compiler(&mut self) -> RefMut<Compiler> {
|
||||
self.compiler.borrow_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Context {
|
||||
fn hash<H>(&self, state: &mut H)
|
||||
where
|
||||
H: Hasher,
|
||||
{
|
||||
self.compiler.as_ptr().hash(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Context {}
|
||||
|
||||
impl PartialEq for Context {
|
||||
fn eq(&self, other: &Context) -> bool {
|
||||
Rc::ptr_eq(&self.compiler, &other.compiler)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn create_compiler(flags: settings::Flags, strategy: CompilationStrategy) -> Compiler {
|
||||
let isa = {
|
||||
let isa_builder =
|
||||
cranelift_native::builder().expect("host machine is not a supported target");
|
||||
isa_builder.finish(flags)
|
||||
};
|
||||
|
||||
Compiler::new(isa, strategy)
|
||||
}
|
||||
490
crates/api/src/externals.rs
Normal file
490
crates/api/src/externals.rs
Normal file
@@ -0,0 +1,490 @@
|
||||
use crate::callable::{Callable, NativeCallable, WasmtimeFn, WrappedCallable};
|
||||
use crate::r#ref::{AnyRef, HostRef};
|
||||
use crate::runtime::Store;
|
||||
use crate::trampoline::{generate_global_export, generate_memory_export, generate_table_export};
|
||||
use crate::trap::Trap;
|
||||
use crate::types::{ExternType, FuncType, GlobalType, MemoryType, TableType, ValType};
|
||||
use crate::values::{from_checked_anyfunc, into_checked_anyfunc, Val};
|
||||
use alloc::boxed::Box;
|
||||
use alloc::rc::Rc;
|
||||
use core::result::Result;
|
||||
use core::slice;
|
||||
use wasmtime_runtime::InstanceHandle;
|
||||
|
||||
// Externals
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Extern {
|
||||
Func(HostRef<Func>),
|
||||
Global(HostRef<Global>),
|
||||
Table(HostRef<Table>),
|
||||
Memory(HostRef<Memory>),
|
||||
}
|
||||
|
||||
impl Extern {
|
||||
pub fn func(&self) -> Option<&HostRef<Func>> {
|
||||
match self {
|
||||
Extern::Func(func) => Some(func),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn global(&self) -> Option<&HostRef<Global>> {
|
||||
match self {
|
||||
Extern::Global(global) => Some(global),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn table(&self) -> Option<&HostRef<Table>> {
|
||||
match self {
|
||||
Extern::Table(table) => Some(table),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn memory(&self) -> Option<&HostRef<Memory>> {
|
||||
match self {
|
||||
Extern::Memory(memory) => Some(memory),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn r#type(&self) -> ExternType {
|
||||
match self {
|
||||
Extern::Func(ft) => ExternType::ExternFunc(ft.borrow().r#type().clone()),
|
||||
Extern::Memory(ft) => ExternType::ExternMemory(ft.borrow().r#type().clone()),
|
||||
Extern::Table(tt) => ExternType::ExternTable(tt.borrow().r#type().clone()),
|
||||
Extern::Global(gt) => ExternType::ExternGlobal(gt.borrow().r#type().clone()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_wasmtime_export(&mut self) -> wasmtime_runtime::Export {
|
||||
match self {
|
||||
Extern::Func(f) => f.borrow().wasmtime_export().clone(),
|
||||
Extern::Global(g) => g.borrow().wasmtime_export().clone(),
|
||||
Extern::Memory(m) => m.borrow().wasmtime_export().clone(),
|
||||
Extern::Table(t) => t.borrow().wasmtime_export().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_wasmtime_export(
|
||||
store: &HostRef<Store>,
|
||||
instance_handle: InstanceHandle,
|
||||
export: wasmtime_runtime::Export,
|
||||
) -> Extern {
|
||||
match export {
|
||||
wasmtime_runtime::Export::Function { .. } => Extern::Func(HostRef::new(
|
||||
Func::from_wasmtime_function(export, store, instance_handle),
|
||||
)),
|
||||
wasmtime_runtime::Export::Memory { .. } => Extern::Memory(HostRef::new(
|
||||
Memory::from_wasmtime_memory(export, store, instance_handle),
|
||||
)),
|
||||
wasmtime_runtime::Export::Global { .. } => {
|
||||
Extern::Global(HostRef::new(Global::from_wasmtime_global(export, store)))
|
||||
}
|
||||
wasmtime_runtime::Export::Table { .. } => Extern::Table(HostRef::new(
|
||||
Table::from_wasmtime_table(export, store, instance_handle),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HostRef<Func>> for Extern {
|
||||
fn from(r: HostRef<Func>) -> Self {
|
||||
Extern::Func(r)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HostRef<Global>> for Extern {
|
||||
fn from(r: HostRef<Global>) -> Self {
|
||||
Extern::Global(r)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HostRef<Memory>> for Extern {
|
||||
fn from(r: HostRef<Memory>) -> Self {
|
||||
Extern::Memory(r)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HostRef<Table>> for Extern {
|
||||
fn from(r: HostRef<Table>) -> Self {
|
||||
Extern::Table(r)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Func {
|
||||
_store: HostRef<Store>,
|
||||
callable: Rc<dyn WrappedCallable + 'static>,
|
||||
r#type: FuncType,
|
||||
}
|
||||
|
||||
impl Func {
|
||||
pub fn new(store: &HostRef<Store>, ty: FuncType, callable: Rc<dyn Callable + 'static>) -> Self {
|
||||
let callable = Rc::new(NativeCallable::new(callable, &ty, &store));
|
||||
Func::from_wrapped(store, ty, callable)
|
||||
}
|
||||
|
||||
fn from_wrapped(
|
||||
store: &HostRef<Store>,
|
||||
r#type: FuncType,
|
||||
callable: Rc<dyn WrappedCallable + 'static>,
|
||||
) -> Func {
|
||||
Func {
|
||||
_store: store.clone(),
|
||||
callable,
|
||||
r#type,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn r#type(&self) -> &FuncType {
|
||||
&self.r#type
|
||||
}
|
||||
|
||||
pub fn param_arity(&self) -> usize {
|
||||
self.r#type.params().len()
|
||||
}
|
||||
|
||||
pub fn result_arity(&self) -> usize {
|
||||
self.r#type.results().len()
|
||||
}
|
||||
|
||||
pub fn call(&self, params: &[Val]) -> Result<Box<[Val]>, HostRef<Trap>> {
|
||||
let mut results = vec![Val::default(); self.result_arity()];
|
||||
self.callable.call(params, &mut results)?;
|
||||
Ok(results.into_boxed_slice())
|
||||
}
|
||||
|
||||
pub(crate) fn wasmtime_export(&self) -> &wasmtime_runtime::Export {
|
||||
self.callable.wasmtime_export()
|
||||
}
|
||||
|
||||
pub(crate) fn from_wasmtime_function(
|
||||
export: wasmtime_runtime::Export,
|
||||
store: &HostRef<Store>,
|
||||
instance_handle: InstanceHandle,
|
||||
) -> Self {
|
||||
let ty = if let wasmtime_runtime::Export::Function { signature, .. } = &export {
|
||||
FuncType::from_cranelift_signature(signature.clone())
|
||||
} else {
|
||||
panic!("expected function export")
|
||||
};
|
||||
let callable = WasmtimeFn::new(store, instance_handle, export.clone());
|
||||
Func::from_wrapped(store, ty, Rc::new(callable))
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for Func {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(f, "Func")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Global {
|
||||
_store: HostRef<Store>,
|
||||
r#type: GlobalType,
|
||||
wasmtime_export: wasmtime_runtime::Export,
|
||||
#[allow(dead_code)]
|
||||
wasmtime_state: Option<crate::trampoline::GlobalState>,
|
||||
}
|
||||
|
||||
impl Global {
|
||||
pub fn new(store: &HostRef<Store>, r#type: GlobalType, val: Val) -> Global {
|
||||
let (wasmtime_export, wasmtime_state) =
|
||||
generate_global_export(&r#type, val).expect("generated global");
|
||||
Global {
|
||||
_store: store.clone(),
|
||||
r#type,
|
||||
wasmtime_export,
|
||||
wasmtime_state: Some(wasmtime_state),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn r#type(&self) -> &GlobalType {
|
||||
&self.r#type
|
||||
}
|
||||
|
||||
fn wasmtime_global_definition(&self) -> *mut wasmtime_runtime::VMGlobalDefinition {
|
||||
match self.wasmtime_export {
|
||||
wasmtime_runtime::Export::Global { definition, .. } => definition,
|
||||
_ => panic!("global definition not found"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self) -> Val {
|
||||
let definition = unsafe { &mut *self.wasmtime_global_definition() };
|
||||
unsafe {
|
||||
match self.r#type().content() {
|
||||
ValType::I32 => Val::from(*definition.as_i32()),
|
||||
ValType::I64 => Val::from(*definition.as_i64()),
|
||||
ValType::F32 => Val::from_f32_bits(*definition.as_u32()),
|
||||
ValType::F64 => Val::from_f64_bits(*definition.as_u64()),
|
||||
_ => unimplemented!("Global::get for {:?}", self.r#type().content()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set(&mut self, val: Val) {
|
||||
if val.r#type() != *self.r#type().content() {
|
||||
panic!(
|
||||
"global of type {:?} cannot be set to {:?}",
|
||||
self.r#type().content(),
|
||||
val.r#type()
|
||||
);
|
||||
}
|
||||
let definition = unsafe { &mut *self.wasmtime_global_definition() };
|
||||
unsafe {
|
||||
match val {
|
||||
Val::I32(i) => *definition.as_i32_mut() = i,
|
||||
Val::I64(i) => *definition.as_i64_mut() = i,
|
||||
Val::F32(f) => *definition.as_u32_mut() = f,
|
||||
Val::F64(f) => *definition.as_u64_mut() = f,
|
||||
_ => unimplemented!("Global::set for {:?}", val.r#type()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn wasmtime_export(&self) -> &wasmtime_runtime::Export {
|
||||
&self.wasmtime_export
|
||||
}
|
||||
|
||||
pub(crate) fn from_wasmtime_global(
|
||||
export: wasmtime_runtime::Export,
|
||||
store: &HostRef<Store>,
|
||||
) -> Global {
|
||||
let global = if let wasmtime_runtime::Export::Global { ref global, .. } = export {
|
||||
global
|
||||
} else {
|
||||
panic!("wasmtime export is not memory")
|
||||
};
|
||||
let ty = GlobalType::from_cranelift_global(global.clone());
|
||||
Global {
|
||||
_store: store.clone(),
|
||||
r#type: ty,
|
||||
wasmtime_export: export,
|
||||
wasmtime_state: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Table {
|
||||
store: HostRef<Store>,
|
||||
r#type: TableType,
|
||||
wasmtime_handle: InstanceHandle,
|
||||
wasmtime_export: wasmtime_runtime::Export,
|
||||
}
|
||||
|
||||
fn get_table_item(
|
||||
handle: &InstanceHandle,
|
||||
store: &HostRef<Store>,
|
||||
table_index: cranelift_wasm::DefinedTableIndex,
|
||||
item_index: u32,
|
||||
) -> Val {
|
||||
if let Some(item) = handle.table_get(table_index, item_index) {
|
||||
from_checked_anyfunc(item, store)
|
||||
} else {
|
||||
AnyRef::null().into()
|
||||
}
|
||||
}
|
||||
|
||||
fn set_table_item(
|
||||
handle: &mut InstanceHandle,
|
||||
store: &HostRef<Store>,
|
||||
table_index: cranelift_wasm::DefinedTableIndex,
|
||||
item_index: u32,
|
||||
val: Val,
|
||||
) -> bool {
|
||||
let item = into_checked_anyfunc(val, store);
|
||||
if let Some(item_ref) = handle.table_get_mut(table_index, item_index) {
|
||||
*item_ref = item;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Table {
|
||||
pub fn new(store: &HostRef<Store>, r#type: TableType, init: Val) -> Table {
|
||||
match r#type.element() {
|
||||
ValType::FuncRef => (),
|
||||
_ => panic!("table is not for funcref"),
|
||||
}
|
||||
let (mut wasmtime_handle, wasmtime_export) =
|
||||
generate_table_export(&r#type).expect("generated table");
|
||||
|
||||
// Initialize entries with the init value.
|
||||
match wasmtime_export {
|
||||
wasmtime_runtime::Export::Table { definition, .. } => {
|
||||
let index = wasmtime_handle.table_index(unsafe { &*definition });
|
||||
let len = unsafe { (*definition).current_elements };
|
||||
for i in 0..len {
|
||||
let _success =
|
||||
set_table_item(&mut wasmtime_handle, store, index, i, init.clone());
|
||||
assert!(_success);
|
||||
}
|
||||
}
|
||||
_ => panic!("global definition not found"),
|
||||
}
|
||||
|
||||
Table {
|
||||
store: store.clone(),
|
||||
r#type,
|
||||
wasmtime_handle,
|
||||
wasmtime_export,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn r#type(&self) -> &TableType {
|
||||
&self.r#type
|
||||
}
|
||||
|
||||
fn wasmtime_table_index(&self) -> cranelift_wasm::DefinedTableIndex {
|
||||
match self.wasmtime_export {
|
||||
wasmtime_runtime::Export::Table { definition, .. } => {
|
||||
self.wasmtime_handle.table_index(unsafe { &*definition })
|
||||
}
|
||||
_ => panic!("global definition not found"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, index: u32) -> Val {
|
||||
let table_index = self.wasmtime_table_index();
|
||||
get_table_item(&self.wasmtime_handle, &self.store, table_index, index)
|
||||
}
|
||||
|
||||
pub fn set(&self, index: u32, val: Val) -> bool {
|
||||
let table_index = self.wasmtime_table_index();
|
||||
let mut wasmtime_handle = self.wasmtime_handle.clone();
|
||||
set_table_item(&mut wasmtime_handle, &self.store, table_index, index, val)
|
||||
}
|
||||
|
||||
pub fn size(&self) -> u32 {
|
||||
match self.wasmtime_export {
|
||||
wasmtime_runtime::Export::Table { definition, .. } => unsafe {
|
||||
(*definition).current_elements
|
||||
},
|
||||
_ => panic!("global definition not found"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn grow(&mut self, delta: u32, init: Val) -> bool {
|
||||
let index = self.wasmtime_table_index();
|
||||
if let Some(len) = self.wasmtime_handle.table_grow(index, delta) {
|
||||
let mut wasmtime_handle = self.wasmtime_handle.clone();
|
||||
for i in 0..delta {
|
||||
let i = len - (delta - i);
|
||||
let _success =
|
||||
set_table_item(&mut wasmtime_handle, &self.store, index, i, init.clone());
|
||||
assert!(_success);
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn wasmtime_export(&self) -> &wasmtime_runtime::Export {
|
||||
&self.wasmtime_export
|
||||
}
|
||||
|
||||
pub(crate) fn from_wasmtime_table(
|
||||
export: wasmtime_runtime::Export,
|
||||
store: &HostRef<Store>,
|
||||
instance_handle: wasmtime_runtime::InstanceHandle,
|
||||
) -> Table {
|
||||
let table = if let wasmtime_runtime::Export::Table { ref table, .. } = export {
|
||||
table
|
||||
} else {
|
||||
panic!("wasmtime export is not table")
|
||||
};
|
||||
let ty = TableType::from_cranelift_table(table.table.clone());
|
||||
Table {
|
||||
store: store.clone(),
|
||||
r#type: ty,
|
||||
wasmtime_handle: instance_handle,
|
||||
wasmtime_export: export,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Memory {
|
||||
_store: HostRef<Store>,
|
||||
r#type: MemoryType,
|
||||
wasmtime_handle: InstanceHandle,
|
||||
wasmtime_export: wasmtime_runtime::Export,
|
||||
}
|
||||
|
||||
impl Memory {
|
||||
pub fn new(store: &HostRef<Store>, r#type: MemoryType) -> Memory {
|
||||
let (wasmtime_handle, wasmtime_export) =
|
||||
generate_memory_export(&r#type).expect("generated memory");
|
||||
Memory {
|
||||
_store: store.clone(),
|
||||
r#type,
|
||||
wasmtime_handle,
|
||||
wasmtime_export,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn r#type(&self) -> &MemoryType {
|
||||
&self.r#type
|
||||
}
|
||||
|
||||
fn wasmtime_memory_definition(&self) -> *mut wasmtime_runtime::VMMemoryDefinition {
|
||||
match self.wasmtime_export {
|
||||
wasmtime_runtime::Export::Memory { definition, .. } => definition,
|
||||
_ => panic!("memory definition not found"),
|
||||
}
|
||||
}
|
||||
|
||||
// Marked unsafe due to posibility that wasmtime can resize internal memory
|
||||
// from other threads.
|
||||
pub unsafe fn data(&self) -> &mut [u8] {
|
||||
let definition = &*self.wasmtime_memory_definition();
|
||||
slice::from_raw_parts_mut(definition.base, definition.current_length)
|
||||
}
|
||||
|
||||
pub fn data_ptr(&self) -> *mut u8 {
|
||||
unsafe { (*self.wasmtime_memory_definition()).base }
|
||||
}
|
||||
|
||||
pub fn data_size(&self) -> usize {
|
||||
unsafe { (*self.wasmtime_memory_definition()).current_length }
|
||||
}
|
||||
|
||||
pub fn size(&self) -> u32 {
|
||||
(self.data_size() / wasmtime_environ::WASM_PAGE_SIZE as usize) as u32
|
||||
}
|
||||
|
||||
pub fn grow(&mut self, delta: u32) -> bool {
|
||||
match self.wasmtime_export {
|
||||
wasmtime_runtime::Export::Memory { definition, .. } => {
|
||||
let definition = unsafe { &(*definition) };
|
||||
let index = self.wasmtime_handle.memory_index(definition);
|
||||
self.wasmtime_handle.memory_grow(index, delta).is_some()
|
||||
}
|
||||
_ => panic!("memory definition not found"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn wasmtime_export(&self) -> &wasmtime_runtime::Export {
|
||||
&self.wasmtime_export
|
||||
}
|
||||
|
||||
pub(crate) fn from_wasmtime_memory(
|
||||
export: wasmtime_runtime::Export,
|
||||
store: &HostRef<Store>,
|
||||
instance_handle: wasmtime_runtime::InstanceHandle,
|
||||
) -> Memory {
|
||||
let memory = if let wasmtime_runtime::Export::Memory { ref memory, .. } = export {
|
||||
memory
|
||||
} else {
|
||||
panic!("wasmtime export is not memory")
|
||||
};
|
||||
let ty = MemoryType::from_cranelift_memory(memory.memory.clone());
|
||||
Memory {
|
||||
_store: store.clone(),
|
||||
r#type: ty,
|
||||
wasmtime_handle: instance_handle,
|
||||
wasmtime_export: export,
|
||||
}
|
||||
}
|
||||
}
|
||||
148
crates/api/src/instance.rs
Normal file
148
crates/api/src/instance.rs
Normal file
@@ -0,0 +1,148 @@
|
||||
use crate::context::Context;
|
||||
use crate::externals::Extern;
|
||||
use crate::module::Module;
|
||||
use crate::r#ref::HostRef;
|
||||
use crate::runtime::Store;
|
||||
use crate::{HashMap, HashSet};
|
||||
use alloc::borrow::ToOwned;
|
||||
use alloc::boxed::Box;
|
||||
use alloc::rc::Rc;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
use anyhow::Result;
|
||||
use core::cell::RefCell;
|
||||
|
||||
use wasmtime_jit::{instantiate, Resolver};
|
||||
use wasmtime_runtime::{Export, InstanceHandle};
|
||||
|
||||
struct SimpleResolver {
|
||||
imports: Vec<(String, String, Extern)>,
|
||||
}
|
||||
|
||||
impl Resolver for SimpleResolver {
|
||||
fn resolve(&mut self, name: &str, field: &str) -> Option<Export> {
|
||||
// TODO speedup lookup
|
||||
self.imports
|
||||
.iter_mut()
|
||||
.find(|(n, f, _)| name == n && field == f)
|
||||
.map(|(_, _, e)| e.get_wasmtime_export())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn instantiate_in_context(
|
||||
data: &[u8],
|
||||
imports: Vec<(String, String, Extern)>,
|
||||
mut context: Context,
|
||||
exports: Rc<RefCell<HashMap<String, Option<wasmtime_runtime::Export>>>>,
|
||||
) -> Result<(InstanceHandle, HashSet<Context>)> {
|
||||
let mut contexts = HashSet::new();
|
||||
let debug_info = context.debug_info();
|
||||
let mut resolver = SimpleResolver { imports };
|
||||
let instance = instantiate(
|
||||
&mut context.compiler(),
|
||||
data,
|
||||
&mut resolver,
|
||||
exports,
|
||||
debug_info,
|
||||
)?;
|
||||
contexts.insert(context);
|
||||
Ok((instance, contexts))
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Instance {
|
||||
instance_handle: InstanceHandle,
|
||||
|
||||
// We need to keep CodeMemory alive.
|
||||
contexts: HashSet<Context>,
|
||||
|
||||
exports: Box<[Extern]>,
|
||||
}
|
||||
|
||||
impl Instance {
|
||||
pub fn new(
|
||||
store: &HostRef<Store>,
|
||||
module: &HostRef<Module>,
|
||||
externs: &[Extern],
|
||||
) -> Result<Instance> {
|
||||
let context = store.borrow_mut().context().clone();
|
||||
let exports = store.borrow_mut().global_exports().clone();
|
||||
let imports = module
|
||||
.borrow()
|
||||
.imports()
|
||||
.iter()
|
||||
.zip(externs.iter())
|
||||
.map(|(i, e)| (i.module().to_string(), i.name().to_string(), e.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
let (mut instance_handle, contexts) =
|
||||
instantiate_in_context(module.borrow().binary(), imports, context, exports)?;
|
||||
|
||||
let exports = {
|
||||
let module = module.borrow();
|
||||
let mut exports = Vec::with_capacity(module.exports().len());
|
||||
for export in module.exports() {
|
||||
let name = export.name().to_string();
|
||||
let export = instance_handle.lookup(&name).expect("export");
|
||||
exports.push(Extern::from_wasmtime_export(
|
||||
store,
|
||||
instance_handle.clone(),
|
||||
export,
|
||||
));
|
||||
}
|
||||
exports.into_boxed_slice()
|
||||
};
|
||||
Ok(Instance {
|
||||
instance_handle,
|
||||
contexts,
|
||||
exports,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn exports(&self) -> &[Extern] {
|
||||
&self.exports
|
||||
}
|
||||
|
||||
pub fn from_handle(
|
||||
store: &HostRef<Store>,
|
||||
instance_handle: InstanceHandle,
|
||||
) -> Result<(Instance, HashMap<String, usize>)> {
|
||||
let contexts = HashSet::new();
|
||||
|
||||
let mut exports = Vec::new();
|
||||
let mut export_names_map = HashMap::new();
|
||||
let mut mutable = instance_handle.clone();
|
||||
for (name, _) in instance_handle.clone().exports() {
|
||||
let export = mutable.lookup(name).expect("export");
|
||||
if let wasmtime_runtime::Export::Function { signature, .. } = &export {
|
||||
// HACK ensure all handles, instantiated outside Store, present in
|
||||
// the store's SignatureRegistry, e.g. WASI instances that are
|
||||
// imported into this store using the from_handle() method.
|
||||
let _ = store.borrow_mut().register_cranelift_signature(signature);
|
||||
}
|
||||
export_names_map.insert(name.to_owned(), exports.len());
|
||||
exports.push(Extern::from_wasmtime_export(
|
||||
store,
|
||||
instance_handle.clone(),
|
||||
export.clone(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok((
|
||||
Instance {
|
||||
instance_handle,
|
||||
contexts,
|
||||
exports: exports.into_boxed_slice(),
|
||||
},
|
||||
export_names_map,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn handle(&self) -> &InstanceHandle {
|
||||
&self.instance_handle
|
||||
}
|
||||
|
||||
pub fn get_wasmtime_memory(&self) -> Option<wasmtime_runtime::Export> {
|
||||
let mut instance_handle = self.instance_handle.clone();
|
||||
instance_handle.lookup("memory")
|
||||
}
|
||||
}
|
||||
35
crates/api/src/lib.rs
Normal file
35
crates/api/src/lib.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
//! Wasmtime embed API. Based on wasm-c-api.
|
||||
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
mod callable;
|
||||
mod context;
|
||||
mod externals;
|
||||
mod instance;
|
||||
mod module;
|
||||
mod r#ref;
|
||||
mod runtime;
|
||||
mod trampoline;
|
||||
mod trap;
|
||||
mod types;
|
||||
mod values;
|
||||
|
||||
pub mod wasm;
|
||||
|
||||
#[macro_use]
|
||||
extern crate alloc;
|
||||
|
||||
pub use crate::callable::Callable;
|
||||
pub use crate::externals::*;
|
||||
pub use crate::instance::Instance;
|
||||
pub use crate::module::Module;
|
||||
pub use crate::r#ref::{AnyRef, HostInfo, HostRef};
|
||||
pub use crate::runtime::{Config, Engine, Store};
|
||||
pub use crate::trap::Trap;
|
||||
pub use crate::types::*;
|
||||
pub use crate::values::*;
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
use hashbrown::{hash_map, HashMap, HashSet};
|
||||
#[cfg(feature = "std")]
|
||||
use std::collections::{hash_map, HashMap, HashSet};
|
||||
206
crates/api/src/module.rs
Normal file
206
crates/api/src/module.rs
Normal file
@@ -0,0 +1,206 @@
|
||||
use crate::r#ref::HostRef;
|
||||
use crate::runtime::Store;
|
||||
use crate::types::{
|
||||
ExportType, ExternType, FuncType, GlobalType, ImportType, Limits, MemoryType, Mutability,
|
||||
TableType, ValType,
|
||||
};
|
||||
use alloc::boxed::Box;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use anyhow::Result;
|
||||
|
||||
use wasmparser::{validate, ExternalKind, ImportSectionEntryType, ModuleReader, SectionCode};
|
||||
|
||||
fn into_memory_type(mt: wasmparser::MemoryType) -> MemoryType {
|
||||
assert!(!mt.shared);
|
||||
MemoryType::new(Limits::new(
|
||||
mt.limits.initial,
|
||||
mt.limits.maximum.unwrap_or(::core::u32::MAX),
|
||||
))
|
||||
}
|
||||
|
||||
fn into_global_type(gt: &wasmparser::GlobalType) -> GlobalType {
|
||||
let mutability = if gt.mutable {
|
||||
Mutability::Var
|
||||
} else {
|
||||
Mutability::Const
|
||||
};
|
||||
GlobalType::new(into_valtype(>.content_type), mutability)
|
||||
}
|
||||
|
||||
fn into_valtype(ty: &wasmparser::Type) -> ValType {
|
||||
use wasmparser::Type::*;
|
||||
match ty {
|
||||
I32 => ValType::I32,
|
||||
I64 => ValType::I64,
|
||||
F32 => ValType::F32,
|
||||
F64 => ValType::F64,
|
||||
V128 => ValType::V128,
|
||||
AnyFunc => ValType::FuncRef,
|
||||
AnyRef => ValType::AnyRef,
|
||||
_ => unimplemented!("types in into_valtype"),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_func_type(mt: wasmparser::FuncType) -> FuncType {
|
||||
assert!(mt.form == wasmparser::Type::Func);
|
||||
let params = mt.params.iter().map(into_valtype).collect::<Vec<_>>();
|
||||
let returns = mt.returns.iter().map(into_valtype).collect::<Vec<_>>();
|
||||
FuncType::new(params.into_boxed_slice(), returns.into_boxed_slice())
|
||||
}
|
||||
|
||||
fn into_table_type(tt: wasmparser::TableType) -> TableType {
|
||||
assert!(
|
||||
tt.element_type == wasmparser::Type::AnyFunc || tt.element_type == wasmparser::Type::AnyRef
|
||||
);
|
||||
let ty = into_valtype(&tt.element_type);
|
||||
let limits = Limits::new(
|
||||
tt.limits.initial,
|
||||
tt.limits.maximum.unwrap_or(::core::u32::MAX),
|
||||
);
|
||||
TableType::new(ty, limits)
|
||||
}
|
||||
|
||||
fn read_imports_and_exports(binary: &[u8]) -> Result<(Box<[ImportType]>, Box<[ExportType]>)> {
|
||||
let mut reader = ModuleReader::new(binary)?;
|
||||
let mut imports = Vec::new();
|
||||
let mut exports = Vec::new();
|
||||
let mut memories = Vec::new();
|
||||
let mut tables = Vec::new();
|
||||
let mut func_sig = Vec::new();
|
||||
let mut sigs = Vec::new();
|
||||
let mut globals = Vec::new();
|
||||
while !reader.eof() {
|
||||
let section = reader.read()?;
|
||||
match section.code {
|
||||
SectionCode::Memory => {
|
||||
let section = section.get_memory_section_reader()?;
|
||||
memories.reserve_exact(section.get_count() as usize);
|
||||
for entry in section {
|
||||
memories.push(into_memory_type(entry?));
|
||||
}
|
||||
}
|
||||
SectionCode::Type => {
|
||||
let section = section.get_type_section_reader()?;
|
||||
sigs.reserve_exact(section.get_count() as usize);
|
||||
for entry in section {
|
||||
sigs.push(into_func_type(entry?));
|
||||
}
|
||||
}
|
||||
SectionCode::Function => {
|
||||
let section = section.get_function_section_reader()?;
|
||||
func_sig.reserve_exact(section.get_count() as usize);
|
||||
for entry in section {
|
||||
func_sig.push(entry?);
|
||||
}
|
||||
}
|
||||
SectionCode::Global => {
|
||||
let section = section.get_global_section_reader()?;
|
||||
globals.reserve_exact(section.get_count() as usize);
|
||||
for entry in section {
|
||||
globals.push(into_global_type(&entry?.ty));
|
||||
}
|
||||
}
|
||||
SectionCode::Table => {
|
||||
let section = section.get_table_section_reader()?;
|
||||
tables.reserve_exact(section.get_count() as usize);
|
||||
for entry in section {
|
||||
tables.push(into_table_type(entry?))
|
||||
}
|
||||
}
|
||||
SectionCode::Import => {
|
||||
let section = section.get_import_section_reader()?;
|
||||
imports.reserve_exact(section.get_count() as usize);
|
||||
for entry in section {
|
||||
let entry = entry?;
|
||||
let module = String::from(entry.module).into();
|
||||
let name = String::from(entry.field).into();
|
||||
let r#type = match entry.ty {
|
||||
ImportSectionEntryType::Function(index) => {
|
||||
func_sig.push(index);
|
||||
let sig = &sigs[index as usize];
|
||||
ExternType::ExternFunc(sig.clone())
|
||||
}
|
||||
ImportSectionEntryType::Table(tt) => {
|
||||
let table = into_table_type(tt);
|
||||
tables.push(table.clone());
|
||||
ExternType::ExternTable(table)
|
||||
}
|
||||
ImportSectionEntryType::Memory(mt) => {
|
||||
let memory = into_memory_type(mt);
|
||||
memories.push(memory.clone());
|
||||
ExternType::ExternMemory(memory)
|
||||
}
|
||||
ImportSectionEntryType::Global(gt) => {
|
||||
let global = into_global_type(>);
|
||||
globals.push(global.clone());
|
||||
ExternType::ExternGlobal(global)
|
||||
}
|
||||
};
|
||||
imports.push(ImportType::new(module, name, r#type));
|
||||
}
|
||||
}
|
||||
SectionCode::Export => {
|
||||
let section = section.get_export_section_reader()?;
|
||||
exports.reserve_exact(section.get_count() as usize);
|
||||
for entry in section {
|
||||
let entry = entry?;
|
||||
let name = String::from(entry.field).into();
|
||||
let r#type = match entry.kind {
|
||||
ExternalKind::Function => {
|
||||
let sig_index = func_sig[entry.index as usize] as usize;
|
||||
let sig = &sigs[sig_index];
|
||||
ExternType::ExternFunc(sig.clone())
|
||||
}
|
||||
ExternalKind::Table => {
|
||||
ExternType::ExternTable(tables[entry.index as usize].clone())
|
||||
}
|
||||
ExternalKind::Memory => {
|
||||
ExternType::ExternMemory(memories[entry.index as usize].clone())
|
||||
}
|
||||
ExternalKind::Global => {
|
||||
ExternType::ExternGlobal(globals[entry.index as usize].clone())
|
||||
}
|
||||
};
|
||||
exports.push(ExportType::new(name, r#type));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// skip other sections
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((imports.into_boxed_slice(), exports.into_boxed_slice()))
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Module {
|
||||
store: HostRef<Store>,
|
||||
binary: Box<[u8]>,
|
||||
imports: Box<[ImportType]>,
|
||||
exports: Box<[ExportType]>,
|
||||
}
|
||||
|
||||
impl Module {
|
||||
pub fn new(store: &HostRef<Store>, binary: &[u8]) -> Result<Module> {
|
||||
let (imports, exports) = read_imports_and_exports(binary)?;
|
||||
Ok(Module {
|
||||
store: store.clone(),
|
||||
binary: binary.into(),
|
||||
imports,
|
||||
exports,
|
||||
})
|
||||
}
|
||||
pub(crate) fn binary(&self) -> &[u8] {
|
||||
&self.binary
|
||||
}
|
||||
pub fn validate(_store: &Store, binary: &[u8]) -> bool {
|
||||
validate(binary, None).is_ok()
|
||||
}
|
||||
pub fn imports(&self) -> &[ImportType] {
|
||||
&self.imports
|
||||
}
|
||||
pub fn exports(&self) -> &[ExportType] {
|
||||
&self.exports
|
||||
}
|
||||
}
|
||||
211
crates/api/src/ref.rs
Normal file
211
crates/api/src/ref.rs
Normal file
@@ -0,0 +1,211 @@
|
||||
use alloc::boxed::Box;
|
||||
use alloc::rc::{Rc, Weak};
|
||||
use core::any::Any;
|
||||
use core::cell::{self, RefCell};
|
||||
use core::fmt;
|
||||
|
||||
pub trait HostInfo {
|
||||
fn finalize(&mut self) {}
|
||||
}
|
||||
|
||||
trait InternalRefBase: Any {
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn host_info(&self) -> Option<cell::RefMut<Box<dyn HostInfo>>>;
|
||||
fn set_host_info(&self, info: Option<Box<dyn HostInfo>>);
|
||||
fn ptr_eq(&self, other: &dyn InternalRefBase) -> bool;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct InternalRef(Rc<dyn InternalRefBase>);
|
||||
|
||||
impl InternalRef {
|
||||
pub fn is_ref<T: 'static>(&self) -> bool {
|
||||
let r = self.0.as_any();
|
||||
Any::is::<HostRef<T>>(r)
|
||||
}
|
||||
pub fn get_ref<T: 'static>(&self) -> HostRef<T> {
|
||||
let r = self.0.as_any();
|
||||
r.downcast_ref::<HostRef<T>>()
|
||||
.expect("reference is not T type")
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
|
||||
struct AnyAndHostInfo {
|
||||
any: Box<dyn Any>,
|
||||
host_info: Option<Box<dyn HostInfo>>,
|
||||
}
|
||||
|
||||
impl Drop for AnyAndHostInfo {
|
||||
fn drop(&mut self) {
|
||||
if let Some(info) = &mut self.host_info {
|
||||
info.finalize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OtherRef(Rc<RefCell<AnyAndHostInfo>>);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum AnyRef {
|
||||
Null,
|
||||
Ref(InternalRef),
|
||||
Other(OtherRef),
|
||||
}
|
||||
|
||||
impl AnyRef {
|
||||
pub fn new(data: Box<dyn Any>) -> Self {
|
||||
let info = AnyAndHostInfo {
|
||||
any: data,
|
||||
host_info: None,
|
||||
};
|
||||
AnyRef::Other(OtherRef(Rc::new(RefCell::new(info))))
|
||||
}
|
||||
|
||||
pub fn null() -> Self {
|
||||
AnyRef::Null
|
||||
}
|
||||
|
||||
pub fn data(&self) -> cell::Ref<Box<dyn Any>> {
|
||||
match self {
|
||||
AnyRef::Other(OtherRef(r)) => cell::Ref::map(r.borrow(), |r| &r.any),
|
||||
_ => panic!("expected AnyRef::Other"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ptr_eq(&self, other: &AnyRef) -> bool {
|
||||
match (self, other) {
|
||||
(AnyRef::Null, AnyRef::Null) => true,
|
||||
(AnyRef::Ref(InternalRef(ref a)), AnyRef::Ref(InternalRef(ref b))) => {
|
||||
a.ptr_eq(b.as_ref())
|
||||
}
|
||||
(AnyRef::Other(OtherRef(ref a)), AnyRef::Other(OtherRef(ref b))) => Rc::ptr_eq(a, b),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn host_info(&self) -> Option<cell::RefMut<Box<dyn HostInfo>>> {
|
||||
match self {
|
||||
AnyRef::Null => panic!("null"),
|
||||
AnyRef::Ref(r) => r.0.host_info(),
|
||||
AnyRef::Other(r) => {
|
||||
let info = cell::RefMut::map(r.0.borrow_mut(), |b| &mut b.host_info);
|
||||
if info.is_none() {
|
||||
return None;
|
||||
}
|
||||
Some(cell::RefMut::map(info, |info| info.as_mut().unwrap()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_host_info(&self, info: Option<Box<dyn HostInfo>>) {
|
||||
match self {
|
||||
AnyRef::Null => panic!("null"),
|
||||
AnyRef::Ref(r) => r.0.set_host_info(info),
|
||||
AnyRef::Other(r) => {
|
||||
r.0.borrow_mut().host_info = info;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for AnyRef {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
AnyRef::Null => write!(f, "null"),
|
||||
AnyRef::Ref(_) => write!(f, "anyref"),
|
||||
AnyRef::Other(_) => write!(f, "other ref"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentBox<T> {
|
||||
content: T,
|
||||
host_info: Option<Box<dyn HostInfo>>,
|
||||
anyref_data: Weak<dyn InternalRefBase>,
|
||||
}
|
||||
|
||||
impl<T> Drop for ContentBox<T> {
|
||||
fn drop(&mut self) {
|
||||
if let Some(info) = &mut self.host_info {
|
||||
info.finalize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HostRef<T>(Rc<RefCell<ContentBox<T>>>);
|
||||
|
||||
impl<T: 'static> HostRef<T> {
|
||||
pub fn new(item: T) -> HostRef<T> {
|
||||
let anyref_data: Weak<HostRef<T>> = Weak::new();
|
||||
let content = ContentBox {
|
||||
content: item,
|
||||
host_info: None,
|
||||
anyref_data,
|
||||
};
|
||||
HostRef(Rc::new(RefCell::new(content)))
|
||||
}
|
||||
|
||||
pub fn borrow(&self) -> cell::Ref<T> {
|
||||
cell::Ref::map(self.0.borrow(), |b| &b.content)
|
||||
}
|
||||
|
||||
pub fn borrow_mut(&self) -> cell::RefMut<T> {
|
||||
cell::RefMut::map(self.0.borrow_mut(), |b| &mut b.content)
|
||||
}
|
||||
|
||||
pub fn ptr_eq(&self, other: &HostRef<T>) -> bool {
|
||||
Rc::ptr_eq(&self.0, &other.0)
|
||||
}
|
||||
|
||||
pub fn anyref(&self) -> AnyRef {
|
||||
let r = self.0.borrow_mut().anyref_data.upgrade();
|
||||
if let Some(r) = r {
|
||||
return AnyRef::Ref(InternalRef(r));
|
||||
}
|
||||
let anyref_data: Rc<dyn InternalRefBase> = Rc::new(self.clone());
|
||||
self.0.borrow_mut().anyref_data = Rc::downgrade(&anyref_data);
|
||||
AnyRef::Ref(InternalRef(anyref_data))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> InternalRefBase for HostRef<T> {
|
||||
fn ptr_eq(&self, other: &dyn InternalRefBase) -> bool {
|
||||
if let Some(other) = other.as_any().downcast_ref() {
|
||||
self.ptr_eq(other)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn host_info(&self) -> Option<cell::RefMut<Box<dyn HostInfo>>> {
|
||||
let info = cell::RefMut::map(self.0.borrow_mut(), |b| &mut b.host_info);
|
||||
if info.is_none() {
|
||||
return None;
|
||||
}
|
||||
Some(cell::RefMut::map(info, |info| info.as_mut().unwrap()))
|
||||
}
|
||||
|
||||
fn set_host_info(&self, info: Option<Box<dyn HostInfo>>) {
|
||||
self.0.borrow_mut().host_info = info;
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Clone for HostRef<T> {
|
||||
fn clone(&self) -> HostRef<T> {
|
||||
HostRef(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: fmt::Debug> fmt::Debug for HostRef<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Ref(")?;
|
||||
self.0.borrow().content.fmt(f)?;
|
||||
write!(f, ")")
|
||||
}
|
||||
}
|
||||
154
crates/api/src/runtime.rs
Normal file
154
crates/api/src/runtime.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
use crate::HashMap;
|
||||
use alloc::boxed::Box;
|
||||
use alloc::rc::Rc;
|
||||
use alloc::string::String;
|
||||
use core::cell::RefCell;
|
||||
|
||||
use crate::context::{create_compiler, Context};
|
||||
use crate::r#ref::HostRef;
|
||||
|
||||
use cranelift_codegen::{ir, settings};
|
||||
use wasmtime_jit::{CompilationStrategy, Features};
|
||||
|
||||
// Runtime Environment
|
||||
|
||||
// Configuration
|
||||
|
||||
fn default_flags() -> settings::Flags {
|
||||
let flag_builder = settings::builder();
|
||||
settings::Flags::new(flag_builder)
|
||||
}
|
||||
|
||||
pub struct Config {
|
||||
flags: settings::Flags,
|
||||
features: Features,
|
||||
debug_info: bool,
|
||||
strategy: CompilationStrategy,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn default() -> Config {
|
||||
Config {
|
||||
debug_info: false,
|
||||
features: Default::default(),
|
||||
flags: default_flags(),
|
||||
strategy: CompilationStrategy::Auto,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
flags: settings::Flags,
|
||||
features: Features,
|
||||
debug_info: bool,
|
||||
strategy: CompilationStrategy,
|
||||
) -> Config {
|
||||
Config {
|
||||
flags,
|
||||
features,
|
||||
debug_info,
|
||||
strategy,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn debug_info(&self) -> bool {
|
||||
self.debug_info
|
||||
}
|
||||
|
||||
pub(crate) fn flags(&self) -> &settings::Flags {
|
||||
&self.flags
|
||||
}
|
||||
|
||||
pub(crate) fn features(&self) -> &Features {
|
||||
&self.features
|
||||
}
|
||||
|
||||
pub(crate) fn strategy(&self) -> CompilationStrategy {
|
||||
self.strategy
|
||||
}
|
||||
}
|
||||
|
||||
// Engine
|
||||
|
||||
pub struct Engine {
|
||||
config: Config,
|
||||
}
|
||||
|
||||
impl Engine {
|
||||
pub fn new(config: Config) -> Engine {
|
||||
Engine { config }
|
||||
}
|
||||
|
||||
pub fn default() -> Engine {
|
||||
Engine::new(Config::default())
|
||||
}
|
||||
|
||||
pub(crate) fn config(&self) -> &Config {
|
||||
&self.config
|
||||
}
|
||||
|
||||
pub fn create_wasmtime_context(&self) -> wasmtime_jit::Context {
|
||||
let flags = self.config.flags().clone();
|
||||
wasmtime_jit::Context::new(Box::new(create_compiler(flags, self.config.strategy())))
|
||||
}
|
||||
}
|
||||
|
||||
// Store
|
||||
|
||||
pub struct Store {
|
||||
engine: HostRef<Engine>,
|
||||
context: Context,
|
||||
global_exports: Rc<RefCell<HashMap<String, Option<wasmtime_runtime::Export>>>>,
|
||||
signature_cache: HashMap<wasmtime_runtime::VMSharedSignatureIndex, ir::Signature>,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
pub fn new(engine: &HostRef<Engine>) -> Store {
|
||||
let flags = engine.borrow().config().flags().clone();
|
||||
let features = engine.borrow().config().features().clone();
|
||||
let debug_info = engine.borrow().config().debug_info();
|
||||
let strategy = engine.borrow().config().strategy();
|
||||
Store {
|
||||
engine: engine.clone(),
|
||||
context: Context::create(flags, features, debug_info, strategy),
|
||||
global_exports: Rc::new(RefCell::new(HashMap::new())),
|
||||
signature_cache: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn engine(&self) -> &HostRef<Engine> {
|
||||
&self.engine
|
||||
}
|
||||
|
||||
pub(crate) fn context(&mut self) -> &mut Context {
|
||||
&mut self.context
|
||||
}
|
||||
|
||||
// Specific to wasmtime: hack to pass memory around to wasi
|
||||
pub fn global_exports(
|
||||
&self,
|
||||
) -> &Rc<RefCell<HashMap<String, Option<wasmtime_runtime::Export>>>> {
|
||||
&self.global_exports
|
||||
}
|
||||
|
||||
pub(crate) fn register_cranelift_signature(
|
||||
&mut self,
|
||||
signature: &ir::Signature,
|
||||
) -> wasmtime_runtime::VMSharedSignatureIndex {
|
||||
use crate::hash_map::Entry;
|
||||
let index = self.context().compiler().signatures().register(signature);
|
||||
match self.signature_cache.entry(index) {
|
||||
Entry::Vacant(v) => {
|
||||
v.insert(signature.clone());
|
||||
}
|
||||
Entry::Occupied(_) => (),
|
||||
}
|
||||
index
|
||||
}
|
||||
|
||||
pub(crate) fn lookup_cranelift_signature(
|
||||
&self,
|
||||
type_index: wasmtime_runtime::VMSharedSignatureIndex,
|
||||
) -> Option<&ir::Signature> {
|
||||
self.signature_cache.get(&type_index)
|
||||
}
|
||||
}
|
||||
62
crates/api/src/trampoline/create_handle.rs
Normal file
62
crates/api/src/trampoline/create_handle.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
//! Support for a calling of an imported function.
|
||||
|
||||
use anyhow::Result;
|
||||
use cranelift_entity::PrimaryMap;
|
||||
use cranelift_wasm::DefinedFuncIndex;
|
||||
//use target_lexicon::HOST;
|
||||
use wasmtime_environ::Module;
|
||||
use wasmtime_runtime::{Imports, InstanceHandle, VMFunctionBody};
|
||||
|
||||
use crate::{HashMap, HashSet};
|
||||
use alloc::boxed::Box;
|
||||
use alloc::rc::Rc;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use core::any::Any;
|
||||
use core::cell::{RefCell, RefMut};
|
||||
|
||||
use crate::runtime::Store;
|
||||
|
||||
pub(crate) fn create_handle(
|
||||
module: Module,
|
||||
signature_registry: Option<RefMut<Store>>,
|
||||
finished_functions: PrimaryMap<DefinedFuncIndex, *const VMFunctionBody>,
|
||||
state: Box<dyn Any>,
|
||||
) -> Result<InstanceHandle> {
|
||||
let global_exports: Rc<RefCell<HashMap<String, Option<wasmtime_runtime::Export>>>> =
|
||||
Rc::new(RefCell::new(HashMap::new()));
|
||||
|
||||
let imports = Imports::new(
|
||||
HashSet::new(),
|
||||
PrimaryMap::new(),
|
||||
PrimaryMap::new(),
|
||||
PrimaryMap::new(),
|
||||
PrimaryMap::new(),
|
||||
);
|
||||
let data_initializers = Vec::new();
|
||||
|
||||
// Compute indices into the shared signature table.
|
||||
let signatures = signature_registry
|
||||
.and_then(|mut signature_registry| {
|
||||
Some(
|
||||
module
|
||||
.signatures
|
||||
.values()
|
||||
.map(|sig| signature_registry.register_cranelift_signature(sig))
|
||||
.collect::<PrimaryMap<_, _>>(),
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| PrimaryMap::new());
|
||||
|
||||
Ok(InstanceHandle::new(
|
||||
Rc::new(module),
|
||||
global_exports,
|
||||
finished_functions.into_boxed_slice(),
|
||||
imports,
|
||||
&data_initializers,
|
||||
signatures.into_boxed_slice(),
|
||||
None,
|
||||
state,
|
||||
)
|
||||
.expect("instance"))
|
||||
}
|
||||
291
crates/api/src/trampoline/func.rs
Normal file
291
crates/api/src/trampoline/func.rs
Normal file
@@ -0,0 +1,291 @@
|
||||
//! Support for a calling of an imported function.
|
||||
|
||||
use crate::r#ref::HostRef;
|
||||
use anyhow::Result;
|
||||
use cranelift_codegen::ir::types;
|
||||
use cranelift_codegen::ir::{InstBuilder, StackSlotData, StackSlotKind, TrapCode};
|
||||
use cranelift_codegen::Context;
|
||||
use cranelift_codegen::{binemit, ir, isa};
|
||||
use cranelift_entity::{EntityRef, PrimaryMap};
|
||||
use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext};
|
||||
use cranelift_wasm::{DefinedFuncIndex, FuncIndex};
|
||||
use wasmtime_environ::{CompiledFunction, Export, Module};
|
||||
use wasmtime_jit::CodeMemory;
|
||||
use wasmtime_runtime::{InstanceHandle, VMContext, VMFunctionBody};
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use alloc::rc::Rc;
|
||||
use alloc::string::ToString;
|
||||
use alloc::vec::Vec;
|
||||
use core::cmp;
|
||||
|
||||
use crate::{Callable, FuncType, Store, Trap, Val};
|
||||
|
||||
use super::create_handle::create_handle;
|
||||
|
||||
struct TrampolineState {
|
||||
func: Rc<dyn Callable + 'static>,
|
||||
trap: Option<HostRef<Trap>>,
|
||||
#[allow(dead_code)]
|
||||
code_memory: CodeMemory,
|
||||
}
|
||||
|
||||
unsafe extern "C" fn stub_fn(vmctx: *mut VMContext, call_id: u32, values_vec: *mut i64) -> u32 {
|
||||
let mut instance = InstanceHandle::from_vmctx(vmctx);
|
||||
|
||||
let (args, returns_len) = {
|
||||
let module = instance.module_ref();
|
||||
let signature = &module.signatures[module.functions[FuncIndex::new(call_id as usize)]];
|
||||
|
||||
let mut args = Vec::new();
|
||||
for i in 1..signature.params.len() {
|
||||
args.push(Val::read_value_from(
|
||||
values_vec.offset(i as isize - 1),
|
||||
signature.params[i].value_type,
|
||||
))
|
||||
}
|
||||
(args, signature.returns.len())
|
||||
};
|
||||
|
||||
let mut returns = vec![Val::default(); returns_len];
|
||||
let func = &instance
|
||||
.host_state()
|
||||
.downcast_mut::<TrampolineState>()
|
||||
.expect("state")
|
||||
.func;
|
||||
|
||||
match func.call(&args, &mut returns) {
|
||||
Ok(()) => {
|
||||
for i in 0..returns_len {
|
||||
// TODO check signature.returns[i].value_type ?
|
||||
returns[i].write_value_to(values_vec.offset(i as isize));
|
||||
}
|
||||
0
|
||||
}
|
||||
Err(trap) => {
|
||||
// TODO read custom exception
|
||||
InstanceHandle::from_vmctx(vmctx)
|
||||
.host_state()
|
||||
.downcast_mut::<TrampolineState>()
|
||||
.expect("state")
|
||||
.trap = Some(trap);
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a trampoline for invoking a Callable.
|
||||
fn make_trampoline(
|
||||
isa: &dyn isa::TargetIsa,
|
||||
code_memory: &mut CodeMemory,
|
||||
fn_builder_ctx: &mut FunctionBuilderContext,
|
||||
call_id: u32,
|
||||
signature: &ir::Signature,
|
||||
) -> *const VMFunctionBody {
|
||||
// Mostly reverse copy of the similar method from wasmtime's
|
||||
// wasmtime-jit/src/compiler.rs.
|
||||
let pointer_type = isa.pointer_type();
|
||||
let mut stub_sig = ir::Signature::new(isa.frontend_config().default_call_conv);
|
||||
|
||||
// Add the `vmctx` parameter.
|
||||
stub_sig.params.push(ir::AbiParam::special(
|
||||
pointer_type,
|
||||
ir::ArgumentPurpose::VMContext,
|
||||
));
|
||||
|
||||
// Add the `call_id` parameter.
|
||||
stub_sig.params.push(ir::AbiParam::new(types::I32));
|
||||
|
||||
// Add the `values_vec` parameter.
|
||||
stub_sig.params.push(ir::AbiParam::new(pointer_type));
|
||||
|
||||
// Add error/trap return.
|
||||
stub_sig.returns.push(ir::AbiParam::new(types::I32));
|
||||
|
||||
let values_vec_len = 8 * cmp::max(signature.params.len() - 1, signature.returns.len()) as u32;
|
||||
|
||||
let mut context = Context::new();
|
||||
context.func =
|
||||
ir::Function::with_name_signature(ir::ExternalName::user(0, 0), signature.clone());
|
||||
|
||||
let ss = context.func.create_stack_slot(StackSlotData::new(
|
||||
StackSlotKind::ExplicitSlot,
|
||||
values_vec_len,
|
||||
));
|
||||
let value_size = 8;
|
||||
|
||||
{
|
||||
let mut builder = FunctionBuilder::new(&mut context.func, fn_builder_ctx);
|
||||
let block0 = builder.create_ebb();
|
||||
|
||||
builder.append_ebb_params_for_function_params(block0);
|
||||
builder.switch_to_block(block0);
|
||||
builder.seal_block(block0);
|
||||
|
||||
let values_vec_ptr_val = builder.ins().stack_addr(pointer_type, ss, 0);
|
||||
let mflags = ir::MemFlags::trusted();
|
||||
for i in 1..signature.params.len() {
|
||||
if i == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let val = builder.func.dfg.ebb_params(block0)[i];
|
||||
builder.ins().store(
|
||||
mflags,
|
||||
val,
|
||||
values_vec_ptr_val,
|
||||
((i - 1) * value_size) as i32,
|
||||
);
|
||||
}
|
||||
|
||||
let vmctx_ptr_val = builder.func.dfg.ebb_params(block0)[0];
|
||||
let call_id_val = builder.ins().iconst(types::I32, call_id as i64);
|
||||
|
||||
let callee_args = vec![vmctx_ptr_val, call_id_val, values_vec_ptr_val];
|
||||
|
||||
let new_sig = builder.import_signature(stub_sig.clone());
|
||||
|
||||
let callee_value = builder
|
||||
.ins()
|
||||
.iconst(pointer_type, stub_fn as *const VMFunctionBody as i64);
|
||||
let call = builder
|
||||
.ins()
|
||||
.call_indirect(new_sig, callee_value, &callee_args);
|
||||
|
||||
let call_result = builder.func.dfg.inst_results(call)[0];
|
||||
builder.ins().trapnz(call_result, TrapCode::User(0));
|
||||
|
||||
let mflags = ir::MemFlags::trusted();
|
||||
let mut results = Vec::new();
|
||||
for (i, r) in signature.returns.iter().enumerate() {
|
||||
let load = builder.ins().load(
|
||||
r.value_type,
|
||||
mflags,
|
||||
values_vec_ptr_val,
|
||||
(i * value_size) as i32,
|
||||
);
|
||||
results.push(load);
|
||||
}
|
||||
builder.ins().return_(&results);
|
||||
builder.finalize()
|
||||
}
|
||||
|
||||
let mut code_buf: Vec<u8> = Vec::new();
|
||||
let mut reloc_sink = RelocSink {};
|
||||
let mut trap_sink = binemit::NullTrapSink {};
|
||||
let mut stackmap_sink = binemit::NullStackmapSink {};
|
||||
context
|
||||
.compile_and_emit(
|
||||
isa,
|
||||
&mut code_buf,
|
||||
&mut reloc_sink,
|
||||
&mut trap_sink,
|
||||
&mut stackmap_sink,
|
||||
)
|
||||
.expect("compile_and_emit");
|
||||
|
||||
let mut unwind_info = Vec::new();
|
||||
context.emit_unwind_info(isa, &mut unwind_info);
|
||||
|
||||
code_memory
|
||||
.allocate_for_function(&CompiledFunction {
|
||||
body: code_buf,
|
||||
jt_offsets: context.func.jt_offsets,
|
||||
unwind_info,
|
||||
})
|
||||
.expect("allocate_for_function")
|
||||
.as_ptr()
|
||||
}
|
||||
|
||||
pub fn create_handle_with_function(
|
||||
ft: &FuncType,
|
||||
func: &Rc<dyn Callable + 'static>,
|
||||
store: &HostRef<Store>,
|
||||
) -> Result<InstanceHandle> {
|
||||
let sig = ft.get_cranelift_signature().clone();
|
||||
|
||||
let isa = {
|
||||
let isa_builder =
|
||||
cranelift_native::builder().expect("host machine is not a supported target");
|
||||
let flag_builder = cranelift_codegen::settings::builder();
|
||||
isa_builder.finish(cranelift_codegen::settings::Flags::new(flag_builder))
|
||||
};
|
||||
|
||||
let mut fn_builder_ctx = FunctionBuilderContext::new();
|
||||
let mut module = Module::new();
|
||||
let mut finished_functions: PrimaryMap<DefinedFuncIndex, *const VMFunctionBody> =
|
||||
PrimaryMap::new();
|
||||
let mut code_memory = CodeMemory::new();
|
||||
|
||||
//let pointer_type = types::Type::triple_pointer_type(&HOST);
|
||||
//let call_conv = isa::CallConv::triple_default(&HOST);
|
||||
|
||||
let sig_id = module.signatures.push(sig.clone());
|
||||
let func_id = module.functions.push(sig_id);
|
||||
module
|
||||
.exports
|
||||
.insert("trampoline".to_string(), Export::Function(func_id));
|
||||
let trampoline = make_trampoline(
|
||||
isa.as_ref(),
|
||||
&mut code_memory,
|
||||
&mut fn_builder_ctx,
|
||||
func_id.index() as u32,
|
||||
&sig,
|
||||
);
|
||||
code_memory.publish();
|
||||
|
||||
finished_functions.push(trampoline);
|
||||
|
||||
let trampoline_state = TrampolineState {
|
||||
func: func.clone(),
|
||||
trap: None,
|
||||
code_memory,
|
||||
};
|
||||
|
||||
create_handle(
|
||||
module,
|
||||
Some(store.borrow_mut()),
|
||||
finished_functions,
|
||||
Box::new(trampoline_state),
|
||||
)
|
||||
}
|
||||
|
||||
/// We don't expect trampoline compilation to produce any relocations, so
|
||||
/// this `RelocSink` just asserts that it doesn't recieve any.
|
||||
struct RelocSink {}
|
||||
|
||||
impl binemit::RelocSink for RelocSink {
|
||||
fn reloc_ebb(
|
||||
&mut self,
|
||||
_offset: binemit::CodeOffset,
|
||||
_reloc: binemit::Reloc,
|
||||
_ebb_offset: binemit::CodeOffset,
|
||||
) {
|
||||
panic!("trampoline compilation should not produce ebb relocs");
|
||||
}
|
||||
fn reloc_external(
|
||||
&mut self,
|
||||
_offset: binemit::CodeOffset,
|
||||
_reloc: binemit::Reloc,
|
||||
_name: &ir::ExternalName,
|
||||
_addend: binemit::Addend,
|
||||
) {
|
||||
panic!("trampoline compilation should not produce external symbol relocs");
|
||||
}
|
||||
fn reloc_constant(
|
||||
&mut self,
|
||||
_code_offset: binemit::CodeOffset,
|
||||
_reloc: binemit::Reloc,
|
||||
_constant_offset: ir::ConstantOffset,
|
||||
) {
|
||||
panic!("trampoline compilation should not produce constant relocs");
|
||||
}
|
||||
fn reloc_jt(
|
||||
&mut self,
|
||||
_offset: binemit::CodeOffset,
|
||||
_reloc: binemit::Reloc,
|
||||
_jt: ir::JumpTable,
|
||||
) {
|
||||
panic!("trampoline compilation should not produce jump table relocs");
|
||||
}
|
||||
}
|
||||
46
crates/api/src/trampoline/global.rs
Normal file
46
crates/api/src/trampoline/global.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use alloc::boxed::Box;
|
||||
use anyhow::Result;
|
||||
use cranelift_entity::PrimaryMap;
|
||||
use wasmtime_environ::Module;
|
||||
use wasmtime_runtime::{InstanceHandle, VMGlobalDefinition};
|
||||
|
||||
use super::create_handle::create_handle;
|
||||
use crate::{GlobalType, Mutability, Val};
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct GlobalState {
|
||||
definition: Box<VMGlobalDefinition>,
|
||||
handle: InstanceHandle,
|
||||
}
|
||||
|
||||
pub fn create_global(gt: &GlobalType, val: Val) -> Result<(wasmtime_runtime::Export, GlobalState)> {
|
||||
let mut definition = Box::new(VMGlobalDefinition::new());
|
||||
unsafe {
|
||||
match val {
|
||||
Val::I32(i) => *definition.as_i32_mut() = i,
|
||||
Val::I64(i) => *definition.as_i64_mut() = i,
|
||||
Val::F32(f) => *definition.as_u32_mut() = f,
|
||||
Val::F64(f) => *definition.as_u64_mut() = f,
|
||||
_ => unimplemented!("create_global for {:?}", gt),
|
||||
}
|
||||
}
|
||||
|
||||
let global = cranelift_wasm::Global {
|
||||
ty: gt.content().get_cranelift_type(),
|
||||
mutability: match gt.mutability() {
|
||||
Mutability::Const => false,
|
||||
Mutability::Var => true,
|
||||
},
|
||||
initializer: cranelift_wasm::GlobalInit::Import, // TODO is it right?
|
||||
};
|
||||
let mut handle =
|
||||
create_handle(Module::new(), None, PrimaryMap::new(), Box::new(())).expect("handle");
|
||||
Ok((
|
||||
wasmtime_runtime::Export::Global {
|
||||
definition: definition.as_mut(),
|
||||
vmctx: handle.vmctx_mut_ptr(),
|
||||
global,
|
||||
},
|
||||
GlobalState { definition, handle },
|
||||
))
|
||||
}
|
||||
35
crates/api/src/trampoline/memory.rs
Normal file
35
crates/api/src/trampoline/memory.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use alloc::boxed::Box;
|
||||
use alloc::string::ToString;
|
||||
use anyhow::Result;
|
||||
use cranelift_entity::PrimaryMap;
|
||||
use wasmtime_environ::Module;
|
||||
use wasmtime_runtime::InstanceHandle;
|
||||
|
||||
use super::create_handle::create_handle;
|
||||
use crate::MemoryType;
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
||||
pub fn create_handle_with_memory(memory: &MemoryType) -> Result<InstanceHandle> {
|
||||
let mut module = Module::new();
|
||||
|
||||
let memory = cranelift_wasm::Memory {
|
||||
minimum: memory.limits().min(),
|
||||
maximum: if memory.limits().max() == core::u32::MAX {
|
||||
None
|
||||
} else {
|
||||
Some(memory.limits().max())
|
||||
},
|
||||
shared: false, // TODO
|
||||
};
|
||||
let tunable = Default::default();
|
||||
|
||||
let memory_plan = wasmtime_environ::MemoryPlan::for_memory(memory, &tunable);
|
||||
let memory_id = module.memory_plans.push(memory_plan);
|
||||
module.exports.insert(
|
||||
"memory".to_string(),
|
||||
wasmtime_environ::Export::Memory(memory_id),
|
||||
);
|
||||
|
||||
create_handle(module, None, PrimaryMap::new(), Box::new(()))
|
||||
}
|
||||
52
crates/api/src/trampoline/mod.rs
Normal file
52
crates/api/src/trampoline/mod.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
//! Utility module to create trampolines in/out WebAssembly module.
|
||||
|
||||
mod create_handle;
|
||||
mod func;
|
||||
mod global;
|
||||
mod memory;
|
||||
mod table;
|
||||
|
||||
use crate::r#ref::HostRef;
|
||||
use alloc::rc::Rc;
|
||||
use anyhow::Result;
|
||||
|
||||
use self::func::create_handle_with_function;
|
||||
use self::global::create_global;
|
||||
use self::memory::create_handle_with_memory;
|
||||
use self::table::create_handle_with_table;
|
||||
use super::{Callable, FuncType, GlobalType, MemoryType, Store, TableType, Val};
|
||||
|
||||
pub use self::global::GlobalState;
|
||||
|
||||
pub fn generate_func_export(
|
||||
ft: &FuncType,
|
||||
func: &Rc<dyn Callable + 'static>,
|
||||
store: &HostRef<Store>,
|
||||
) -> Result<(wasmtime_runtime::InstanceHandle, wasmtime_runtime::Export)> {
|
||||
let mut instance = create_handle_with_function(ft, func, store)?;
|
||||
let export = instance.lookup("trampoline").expect("trampoline export");
|
||||
Ok((instance, export))
|
||||
}
|
||||
|
||||
pub fn generate_global_export(
|
||||
gt: &GlobalType,
|
||||
val: Val,
|
||||
) -> Result<(wasmtime_runtime::Export, GlobalState)> {
|
||||
create_global(gt, val)
|
||||
}
|
||||
|
||||
pub fn generate_memory_export(
|
||||
m: &MemoryType,
|
||||
) -> Result<(wasmtime_runtime::InstanceHandle, wasmtime_runtime::Export)> {
|
||||
let mut instance = create_handle_with_memory(m)?;
|
||||
let export = instance.lookup("memory").expect("memory export");
|
||||
Ok((instance, export))
|
||||
}
|
||||
|
||||
pub fn generate_table_export(
|
||||
t: &TableType,
|
||||
) -> Result<(wasmtime_runtime::InstanceHandle, wasmtime_runtime::Export)> {
|
||||
let mut instance = create_handle_with_table(t)?;
|
||||
let export = instance.lookup("table").expect("table export");
|
||||
Ok((instance, export))
|
||||
}
|
||||
37
crates/api/src/trampoline/table.rs
Normal file
37
crates/api/src/trampoline/table.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use alloc::boxed::Box;
|
||||
use alloc::string::ToString;
|
||||
use anyhow::Result;
|
||||
use cranelift_entity::PrimaryMap;
|
||||
use cranelift_wasm::TableElementType;
|
||||
use wasmtime_environ::Module;
|
||||
use wasmtime_runtime::InstanceHandle;
|
||||
|
||||
use super::create_handle::create_handle;
|
||||
use crate::{TableType, ValType};
|
||||
|
||||
pub fn create_handle_with_table(table: &TableType) -> Result<InstanceHandle> {
|
||||
let mut module = Module::new();
|
||||
|
||||
let table = cranelift_wasm::Table {
|
||||
minimum: table.limits().min(),
|
||||
maximum: if table.limits().max() == core::u32::MAX {
|
||||
None
|
||||
} else {
|
||||
Some(table.limits().max())
|
||||
},
|
||||
ty: match table.element() {
|
||||
ValType::FuncRef => TableElementType::Func,
|
||||
_ => TableElementType::Val(table.element().get_cranelift_type()),
|
||||
},
|
||||
};
|
||||
let tunable = Default::default();
|
||||
|
||||
let table_plan = wasmtime_environ::TablePlan::for_table(table, &tunable);
|
||||
let table_id = module.table_plans.push(table_plan);
|
||||
module.exports.insert(
|
||||
"table".to_string(),
|
||||
wasmtime_environ::Export::Table(table_id),
|
||||
);
|
||||
|
||||
create_handle(module, None, PrimaryMap::new(), Box::new(()))
|
||||
}
|
||||
22
crates/api/src/trap.rs
Normal file
22
crates/api/src/trap.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use alloc::string::{String, ToString};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[error("Wasm trap: {message}")]
|
||||
pub struct Trap {
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl Trap {
|
||||
pub fn new(message: String) -> Trap {
|
||||
Trap { message }
|
||||
}
|
||||
|
||||
pub fn fake() -> Trap {
|
||||
Trap::new("TODO trap".to_string())
|
||||
}
|
||||
|
||||
pub fn message(&self) -> &str {
|
||||
&self.message
|
||||
}
|
||||
}
|
||||
353
crates/api/src/types.rs
Normal file
353
crates/api/src/types.rs
Normal file
@@ -0,0 +1,353 @@
|
||||
use alloc::borrow::ToOwned;
|
||||
use alloc::boxed::Box;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use cranelift_codegen::ir;
|
||||
|
||||
// Type Representations
|
||||
|
||||
// Type attributes
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Mutability {
|
||||
Const,
|
||||
Var,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Limits {
|
||||
min: u32,
|
||||
max: u32,
|
||||
}
|
||||
|
||||
impl Limits {
|
||||
pub fn new(min: u32, max: u32) -> Limits {
|
||||
Limits { min, max }
|
||||
}
|
||||
|
||||
pub fn at_least(min: u32) -> Limits {
|
||||
Limits {
|
||||
min,
|
||||
max: ::core::u32::MAX,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn min(&self) -> u32 {
|
||||
self.min
|
||||
}
|
||||
|
||||
pub fn max(&self) -> u32 {
|
||||
self.max
|
||||
}
|
||||
}
|
||||
|
||||
// Value Types
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum ValType {
|
||||
I32,
|
||||
I64,
|
||||
F32,
|
||||
F64,
|
||||
V128,
|
||||
AnyRef, /* = 128 */
|
||||
FuncRef,
|
||||
}
|
||||
|
||||
impl ValType {
|
||||
pub fn is_num(&self) -> bool {
|
||||
match self {
|
||||
ValType::I32 | ValType::I64 | ValType::F32 | ValType::F64 => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_ref(&self) -> bool {
|
||||
match self {
|
||||
ValType::AnyRef | ValType::FuncRef => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_cranelift_type(&self) -> ir::Type {
|
||||
match self {
|
||||
ValType::I32 => ir::types::I32,
|
||||
ValType::I64 => ir::types::I64,
|
||||
ValType::F32 => ir::types::F32,
|
||||
ValType::F64 => ir::types::F64,
|
||||
ValType::V128 => ir::types::I8X16,
|
||||
_ => unimplemented!("get_cranelift_type other"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_cranelift_type(ty: ir::Type) -> ValType {
|
||||
match ty {
|
||||
ir::types::I32 => ValType::I32,
|
||||
ir::types::I64 => ValType::I64,
|
||||
ir::types::F32 => ValType::F32,
|
||||
ir::types::F64 => ValType::F64,
|
||||
ir::types::I8X16 => ValType::V128,
|
||||
_ => unimplemented!("from_cranelift_type other"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// External Types
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExternType {
|
||||
ExternFunc(FuncType),
|
||||
ExternGlobal(GlobalType),
|
||||
ExternTable(TableType),
|
||||
ExternMemory(MemoryType),
|
||||
}
|
||||
|
||||
impl ExternType {
|
||||
pub fn func(&self) -> &FuncType {
|
||||
match self {
|
||||
ExternType::ExternFunc(func) => func,
|
||||
_ => panic!("ExternType::ExternFunc expected"),
|
||||
}
|
||||
}
|
||||
pub fn global(&self) -> &GlobalType {
|
||||
match self {
|
||||
ExternType::ExternGlobal(func) => func,
|
||||
_ => panic!("ExternType::ExternGlobal expected"),
|
||||
}
|
||||
}
|
||||
pub fn table(&self) -> &TableType {
|
||||
match self {
|
||||
ExternType::ExternTable(table) => table,
|
||||
_ => panic!("ExternType::ExternTable expected"),
|
||||
}
|
||||
}
|
||||
pub fn memory(&self) -> &MemoryType {
|
||||
match self {
|
||||
ExternType::ExternMemory(memory) => memory,
|
||||
_ => panic!("ExternType::ExternMemory expected"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function Types
|
||||
fn from_cranelift_abiparam(param: &ir::AbiParam) -> ValType {
|
||||
assert!(param.purpose == ir::ArgumentPurpose::Normal);
|
||||
ValType::from_cranelift_type(param.value_type)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FuncType {
|
||||
params: Box<[ValType]>,
|
||||
results: Box<[ValType]>,
|
||||
signature: ir::Signature,
|
||||
}
|
||||
|
||||
impl FuncType {
|
||||
pub fn new(params: Box<[ValType]>, results: Box<[ValType]>) -> FuncType {
|
||||
use cranelift_codegen::ir::*;
|
||||
use cranelift_codegen::isa::CallConv;
|
||||
use target_lexicon::HOST;
|
||||
let call_conv = CallConv::triple_default(&HOST);
|
||||
let signature: Signature = {
|
||||
let mut params = params
|
||||
.iter()
|
||||
.map(|p| AbiParam::new(p.get_cranelift_type()))
|
||||
.collect::<Vec<_>>();
|
||||
let returns = results
|
||||
.iter()
|
||||
.map(|p| AbiParam::new(p.get_cranelift_type()))
|
||||
.collect::<Vec<_>>();
|
||||
params.insert(0, AbiParam::special(types::I64, ArgumentPurpose::VMContext));
|
||||
|
||||
Signature {
|
||||
params,
|
||||
returns,
|
||||
call_conv,
|
||||
}
|
||||
};
|
||||
FuncType {
|
||||
params,
|
||||
results,
|
||||
signature,
|
||||
}
|
||||
}
|
||||
pub fn params(&self) -> &[ValType] {
|
||||
&self.params
|
||||
}
|
||||
pub fn results(&self) -> &[ValType] {
|
||||
&self.results
|
||||
}
|
||||
|
||||
pub(crate) fn get_cranelift_signature(&self) -> &ir::Signature {
|
||||
&self.signature
|
||||
}
|
||||
|
||||
pub(crate) fn from_cranelift_signature(signature: ir::Signature) -> FuncType {
|
||||
let params = signature
|
||||
.params
|
||||
.iter()
|
||||
.filter(|p| p.purpose == ir::ArgumentPurpose::Normal)
|
||||
.map(|p| from_cranelift_abiparam(p))
|
||||
.collect::<Vec<_>>();
|
||||
let results = signature
|
||||
.returns
|
||||
.iter()
|
||||
.map(|p| from_cranelift_abiparam(p))
|
||||
.collect::<Vec<_>>();
|
||||
FuncType {
|
||||
params: params.into_boxed_slice(),
|
||||
results: results.into_boxed_slice(),
|
||||
signature,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global Types
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GlobalType {
|
||||
content: ValType,
|
||||
mutability: Mutability,
|
||||
}
|
||||
|
||||
impl GlobalType {
|
||||
pub fn new(content: ValType, mutability: Mutability) -> GlobalType {
|
||||
GlobalType {
|
||||
content,
|
||||
mutability,
|
||||
}
|
||||
}
|
||||
pub fn content(&self) -> &ValType {
|
||||
&self.content
|
||||
}
|
||||
pub fn mutability(&self) -> Mutability {
|
||||
self.mutability
|
||||
}
|
||||
|
||||
pub(crate) fn from_cranelift_global(global: cranelift_wasm::Global) -> GlobalType {
|
||||
let ty = ValType::from_cranelift_type(global.ty);
|
||||
let mutability = if global.mutability {
|
||||
Mutability::Var
|
||||
} else {
|
||||
Mutability::Const
|
||||
};
|
||||
GlobalType::new(ty, mutability)
|
||||
}
|
||||
}
|
||||
|
||||
// Table Types
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TableType {
|
||||
element: ValType,
|
||||
limits: Limits,
|
||||
}
|
||||
|
||||
impl TableType {
|
||||
pub fn new(element: ValType, limits: Limits) -> TableType {
|
||||
TableType { element, limits }
|
||||
}
|
||||
pub fn element(&self) -> &ValType {
|
||||
&self.element
|
||||
}
|
||||
pub fn limits(&self) -> &Limits {
|
||||
&self.limits
|
||||
}
|
||||
|
||||
pub(crate) fn from_cranelift_table(table: cranelift_wasm::Table) -> TableType {
|
||||
assert!(if let cranelift_wasm::TableElementType::Func = table.ty {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
});
|
||||
let ty = ValType::FuncRef;
|
||||
let limits = Limits::new(table.minimum, table.maximum.unwrap_or(::core::u32::MAX));
|
||||
TableType::new(ty, limits)
|
||||
}
|
||||
}
|
||||
|
||||
// Memory Types
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MemoryType {
|
||||
limits: Limits,
|
||||
}
|
||||
|
||||
impl MemoryType {
|
||||
pub fn new(limits: Limits) -> MemoryType {
|
||||
MemoryType { limits }
|
||||
}
|
||||
pub fn limits(&self) -> &Limits {
|
||||
&self.limits
|
||||
}
|
||||
|
||||
pub(crate) fn from_cranelift_memory(memory: cranelift_wasm::Memory) -> MemoryType {
|
||||
MemoryType::new(Limits::new(
|
||||
memory.minimum,
|
||||
memory.maximum.unwrap_or(::core::u32::MAX),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// Import Types
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Name(String);
|
||||
|
||||
impl From<String> for Name {
|
||||
fn from(s: String) -> Name {
|
||||
Name(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl ::alloc::string::ToString for Name {
|
||||
fn to_string(&self) -> String {
|
||||
self.0.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ImportType {
|
||||
module: Name,
|
||||
name: Name,
|
||||
r#type: ExternType,
|
||||
}
|
||||
|
||||
impl ImportType {
|
||||
pub fn new(module: Name, name: Name, r#type: ExternType) -> ImportType {
|
||||
ImportType {
|
||||
module,
|
||||
name,
|
||||
r#type,
|
||||
}
|
||||
}
|
||||
pub fn module(&self) -> &Name {
|
||||
&self.module
|
||||
}
|
||||
pub fn name(&self) -> &Name {
|
||||
&self.name
|
||||
}
|
||||
pub fn r#type(&self) -> &ExternType {
|
||||
&self.r#type
|
||||
}
|
||||
}
|
||||
|
||||
// Export Types
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExportType {
|
||||
name: Name,
|
||||
r#type: ExternType,
|
||||
}
|
||||
|
||||
impl ExportType {
|
||||
pub fn new(name: Name, r#type: ExternType) -> ExportType {
|
||||
ExportType { name, r#type }
|
||||
}
|
||||
pub fn name(&self) -> &Name {
|
||||
&self.name
|
||||
}
|
||||
pub fn r#type(&self) -> &ExternType {
|
||||
&self.r#type
|
||||
}
|
||||
}
|
||||
235
crates/api/src/values.rs
Normal file
235
crates/api/src/values.rs
Normal file
@@ -0,0 +1,235 @@
|
||||
use crate::externals::Func;
|
||||
use crate::r#ref::{AnyRef, HostRef};
|
||||
use crate::runtime::Store;
|
||||
use crate::types::ValType;
|
||||
use core::ptr;
|
||||
|
||||
use cranelift_codegen::ir;
|
||||
use wasmtime_jit::RuntimeValue;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Val {
|
||||
I32(i32),
|
||||
I64(i64),
|
||||
F32(u32),
|
||||
F64(u64),
|
||||
AnyRef(AnyRef),
|
||||
FuncRef(HostRef<Func>),
|
||||
}
|
||||
|
||||
impl Val {
|
||||
pub fn default() -> Val {
|
||||
Val::AnyRef(AnyRef::null())
|
||||
}
|
||||
|
||||
pub fn r#type(&self) -> ValType {
|
||||
match self {
|
||||
Val::I32(_) => ValType::I32,
|
||||
Val::I64(_) => ValType::I64,
|
||||
Val::F32(_) => ValType::F32,
|
||||
Val::F64(_) => ValType::F64,
|
||||
Val::AnyRef(_) => ValType::AnyRef,
|
||||
Val::FuncRef(_) => ValType::FuncRef,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) unsafe fn write_value_to(&self, p: *mut i64) {
|
||||
match self {
|
||||
Val::I32(i) => ptr::write(p as *mut i32, *i),
|
||||
Val::I64(i) => ptr::write(p as *mut i64, *i),
|
||||
Val::F32(u) => ptr::write(p as *mut u32, *u),
|
||||
Val::F64(u) => ptr::write(p as *mut u64, *u),
|
||||
_ => unimplemented!("Val::write_value_to"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) unsafe fn read_value_from(p: *const i64, ty: ir::Type) -> Val {
|
||||
match ty {
|
||||
ir::types::I32 => Val::I32(ptr::read(p as *const i32)),
|
||||
ir::types::I64 => Val::I64(ptr::read(p as *const i64)),
|
||||
ir::types::F32 => Val::F32(ptr::read(p as *const u32)),
|
||||
ir::types::F64 => Val::F64(ptr::read(p as *const u64)),
|
||||
_ => unimplemented!("Val::read_value_from"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_f32_bits(v: u32) -> Val {
|
||||
Val::F32(v)
|
||||
}
|
||||
|
||||
pub fn from_f64_bits(v: u64) -> Val {
|
||||
Val::F64(v)
|
||||
}
|
||||
|
||||
pub fn i32(&self) -> i32 {
|
||||
if let Val::I32(i) = self {
|
||||
*i
|
||||
} else {
|
||||
panic!("Invalid conversion of {:?} to i32.", self);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn i64(&self) -> i64 {
|
||||
if let Val::I64(i) = self {
|
||||
*i
|
||||
} else {
|
||||
panic!("Invalid conversion of {:?} to i64.", self);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn f32(&self) -> f32 {
|
||||
RuntimeValue::F32(self.f32_bits()).unwrap_f32()
|
||||
}
|
||||
|
||||
pub fn f64(&self) -> f64 {
|
||||
RuntimeValue::F64(self.f64_bits()).unwrap_f64()
|
||||
}
|
||||
|
||||
pub fn f32_bits(&self) -> u32 {
|
||||
if let Val::F32(i) = self {
|
||||
*i
|
||||
} else {
|
||||
panic!("Invalid conversion of {:?} to f32.", self);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn f64_bits(&self) -> u64 {
|
||||
if let Val::F64(i) = self {
|
||||
*i
|
||||
} else {
|
||||
panic!("Invalid conversion of {:?} to f64.", self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for Val {
|
||||
fn from(val: i32) -> Val {
|
||||
Val::I32(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for Val {
|
||||
fn from(val: i64) -> Val {
|
||||
Val::I64(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f32> for Val {
|
||||
fn from(val: f32) -> Val {
|
||||
Val::F32(val.to_bits())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for Val {
|
||||
fn from(val: f64) -> Val {
|
||||
Val::F64(val.to_bits())
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<i32> for Val {
|
||||
fn into(self) -> i32 {
|
||||
self.i32()
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<i64> for Val {
|
||||
fn into(self) -> i64 {
|
||||
self.i64()
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<f32> for Val {
|
||||
fn into(self) -> f32 {
|
||||
self.f32()
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<f64> for Val {
|
||||
fn into(self) -> f64 {
|
||||
self.f64()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AnyRef> for Val {
|
||||
fn from(val: AnyRef) -> Val {
|
||||
match &val {
|
||||
AnyRef::Ref(r) => {
|
||||
if r.is_ref::<Func>() {
|
||||
Val::FuncRef(r.get_ref())
|
||||
} else {
|
||||
Val::AnyRef(val)
|
||||
}
|
||||
}
|
||||
_ => unimplemented!("AnyRef::Other"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HostRef<Func>> for Val {
|
||||
fn from(val: HostRef<Func>) -> Val {
|
||||
Val::FuncRef(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<AnyRef> for Val {
|
||||
fn into(self) -> AnyRef {
|
||||
match self {
|
||||
Val::AnyRef(r) => r,
|
||||
Val::FuncRef(f) => f.anyref(),
|
||||
_ => panic!("Invalid conversion of {:?} to anyref.", self),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn into_checked_anyfunc(
|
||||
val: Val,
|
||||
store: &HostRef<Store>,
|
||||
) -> wasmtime_runtime::VMCallerCheckedAnyfunc {
|
||||
match val {
|
||||
Val::AnyRef(AnyRef::Null) => wasmtime_runtime::VMCallerCheckedAnyfunc {
|
||||
func_ptr: ptr::null(),
|
||||
type_index: wasmtime_runtime::VMSharedSignatureIndex::default(),
|
||||
vmctx: ptr::null_mut(),
|
||||
},
|
||||
Val::FuncRef(f) => {
|
||||
let f = f.borrow();
|
||||
let (vmctx, func_ptr, signature) = match f.wasmtime_export() {
|
||||
wasmtime_runtime::Export::Function {
|
||||
vmctx,
|
||||
address,
|
||||
signature,
|
||||
} => (*vmctx, *address, signature),
|
||||
_ => panic!("expected function export"),
|
||||
};
|
||||
let type_index = store.borrow_mut().register_cranelift_signature(signature);
|
||||
wasmtime_runtime::VMCallerCheckedAnyfunc {
|
||||
func_ptr,
|
||||
type_index,
|
||||
vmctx,
|
||||
}
|
||||
}
|
||||
_ => panic!("val is not funcref"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_checked_anyfunc(
|
||||
item: &wasmtime_runtime::VMCallerCheckedAnyfunc,
|
||||
store: &HostRef<Store>,
|
||||
) -> Val {
|
||||
if item.type_index == wasmtime_runtime::VMSharedSignatureIndex::default() {
|
||||
return Val::AnyRef(AnyRef::Null);
|
||||
}
|
||||
let signature = store
|
||||
.borrow()
|
||||
.lookup_cranelift_signature(item.type_index)
|
||||
.expect("signature")
|
||||
.clone();
|
||||
let instance_handle = unsafe { wasmtime_runtime::InstanceHandle::from_vmctx(item.vmctx) };
|
||||
let export = wasmtime_runtime::Export::Function {
|
||||
address: item.func_ptr,
|
||||
signature,
|
||||
vmctx: item.vmctx,
|
||||
};
|
||||
let f = Func::from_wasmtime_function(export, store, instance_handle);
|
||||
Val::FuncRef(HostRef::new(f))
|
||||
}
|
||||
1655
crates/api/src/wasm.rs
Normal file
1655
crates/api/src/wasm.rs
Normal file
File diff suppressed because it is too large
Load Diff
39
crates/api/tests/examples.rs
Normal file
39
crates/api/tests/examples.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use std::env;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
fn run_example(name: &'static str) {
|
||||
let cargo = env::var("CARGO").unwrap_or("cargo".to_string());
|
||||
let pkg_dir = env!("CARGO_MANIFEST_DIR");
|
||||
assert!(
|
||||
Command::new(cargo)
|
||||
.current_dir(pkg_dir)
|
||||
.stdout(Stdio::null())
|
||||
.args(&["run", "-q", "--example", name])
|
||||
.status()
|
||||
.expect("success")
|
||||
.success(),
|
||||
"failed to execute the example '{}'",
|
||||
name,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_hello_example() {
|
||||
run_example("hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_gcd_example() {
|
||||
run_example("gcd");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_memory_example() {
|
||||
run_example("memory");
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[test]
|
||||
fn test_run_multi_example() {
|
||||
run_example("multi");
|
||||
}
|
||||
70
crates/api/tests/import_calling_export.rs
Normal file
70
crates/api/tests/import_calling_export.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
extern crate alloc;
|
||||
|
||||
use alloc::rc::Rc;
|
||||
use core::cell::{Ref, RefCell};
|
||||
use std::fs::read;
|
||||
use wasmtime_api::*;
|
||||
|
||||
#[test]
|
||||
fn test_import_calling_export() {
|
||||
struct Callback {
|
||||
pub other: RefCell<Option<HostRef<Func>>>,
|
||||
}
|
||||
|
||||
impl Callable for Callback {
|
||||
fn call(&self, _params: &[Val], _results: &mut [Val]) -> Result<(), HostRef<Trap>> {
|
||||
self.other
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.expect("expected a function ref")
|
||||
.borrow()
|
||||
.call(&[])
|
||||
.expect("expected function not to trap");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
let engine = HostRef::new(Engine::new(Config::default()));
|
||||
let store = HostRef::new(Store::new(&engine));
|
||||
let module = HostRef::new(
|
||||
Module::new(
|
||||
&store,
|
||||
&read("tests/import_calling_export.wasm").expect("failed to read wasm file"),
|
||||
)
|
||||
.expect("failed to create module"),
|
||||
);
|
||||
|
||||
let callback = Rc::new(Callback {
|
||||
other: RefCell::new(None),
|
||||
});
|
||||
|
||||
let callback_func = HostRef::new(Func::new(
|
||||
&store,
|
||||
FuncType::new(Box::new([]), Box::new([])),
|
||||
callback.clone(),
|
||||
));
|
||||
|
||||
let imports = vec![callback_func.into()];
|
||||
let instance = HostRef::new(
|
||||
Instance::new(&store, &module, imports.as_slice()).expect("failed to instantiate module"),
|
||||
);
|
||||
|
||||
let exports = Ref::map(instance.borrow(), |instance| instance.exports());
|
||||
assert!(!exports.is_empty());
|
||||
|
||||
let run_func = exports[0]
|
||||
.func()
|
||||
.expect("expected a run func in the module");
|
||||
|
||||
*callback.other.borrow_mut() = Some(
|
||||
exports[1]
|
||||
.func()
|
||||
.expect("expected an other func in the module")
|
||||
.clone(),
|
||||
);
|
||||
|
||||
run_func
|
||||
.borrow()
|
||||
.call(&[])
|
||||
.expect("expected function not to trap");
|
||||
}
|
||||
BIN
crates/api/tests/import_calling_export.wasm
Normal file
BIN
crates/api/tests/import_calling_export.wasm
Normal file
Binary file not shown.
8
crates/api/tests/import_calling_export.wat
Normal file
8
crates/api/tests/import_calling_export.wat
Normal file
@@ -0,0 +1,8 @@
|
||||
(module
|
||||
(type $t0 (func))
|
||||
(import "" "imp" (func $.imp (type $t0)))
|
||||
(func $run call $.imp)
|
||||
(func $other)
|
||||
(export "run" (func $run))
|
||||
(export "other" (func $other))
|
||||
)
|
||||
3
crates/debug/.gitignore
vendored
Normal file
3
crates/debug/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
target/
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
34
crates/debug/Cargo.toml
Normal file
34
crates/debug/Cargo.toml
Normal file
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "wasmtime-debug"
|
||||
version = "0.2.0"
|
||||
authors = ["The Wasmtime Project Developers"]
|
||||
description = "Debug utils for WebAsssembly code in Cranelift"
|
||||
repository = "https://github.com/CraneStation/wasmtime"
|
||||
documentation = "https://docs.rs/wasmtime-debug/"
|
||||
categories = ["wasm"]
|
||||
keywords = ["webassembly", "wasm", "debuginfo"]
|
||||
license = "Apache-2.0 WITH LLVM-exception"
|
||||
readme = "README.md"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
gimli = "0.19.0"
|
||||
wasmparser = "0.39.2"
|
||||
cranelift-codegen = { version = "0.49", features = ["enable-serde"] }
|
||||
cranelift-entity = { version = "0.49", features = ["enable-serde"] }
|
||||
cranelift-wasm = { version = "0.49", features = ["enable-serde"] }
|
||||
faerie = "0.12.0"
|
||||
wasmtime-environ = { path = "../environ", default-features = false }
|
||||
target-lexicon = { version = "0.9.0", default-features = false }
|
||||
failure = { version = "0.1.3", default-features = false }
|
||||
hashbrown = { version = "0.6.0", optional = true }
|
||||
thiserror = "1.0.4"
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = ["cranelift-codegen/std", "cranelift-wasm/std", "wasmtime-environ/std"]
|
||||
core = ["hashbrown/nightly", "cranelift-codegen/core", "cranelift-wasm/core"]
|
||||
|
||||
[badges]
|
||||
maintenance = { status = "experimental" }
|
||||
travis-ci = { repository = "CraneStation/wasmtime" }
|
||||
220
crates/debug/LICENSE
Normal file
220
crates/debug/LICENSE
Normal file
@@ -0,0 +1,220 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
--- LLVM Exceptions to the Apache 2.0 License ----
|
||||
|
||||
As an exception, if, as a result of your compiling your source code, portions
|
||||
of this Software are embedded into an Object form of such source code, you
|
||||
may redistribute such embedded portions in such Object form without complying
|
||||
with the conditions of Sections 4(a), 4(b) and 4(d) of the License.
|
||||
|
||||
In addition, if you combine or link compiled forms of this Software with
|
||||
software that is licensed under the GPLv2 ("Combined Software") and if a
|
||||
court of competent jurisdiction determines that the patent provision (Section
|
||||
3), the indemnity provision (Section 9) or other Section of the License
|
||||
conflicts with the conditions of the GPLv2, you may retroactively and
|
||||
prospectively choose to deem waived or otherwise exclude such Section(s) of
|
||||
the License, but only in their entirety and only with respect to the Combined
|
||||
Software.
|
||||
|
||||
4
crates/debug/README.md
Normal file
4
crates/debug/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
This is the `wasmtime-debug` crate, which provides functionality to
|
||||
read, transform, and write DWARF section.
|
||||
|
||||
[`wasmtime-debug`]: https://crates.io/crates/wasmtime-debug
|
||||
232
crates/debug/src/gc.rs
Normal file
232
crates/debug/src/gc.rs
Normal file
@@ -0,0 +1,232 @@
|
||||
use crate::transform::AddressTransform;
|
||||
use crate::{HashMap, HashSet};
|
||||
use alloc::vec::Vec;
|
||||
use gimli::constants;
|
||||
use gimli::read;
|
||||
use gimli::{Reader, UnitSectionOffset};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Dependencies {
|
||||
edges: HashMap<UnitSectionOffset, HashSet<UnitSectionOffset>>,
|
||||
roots: HashSet<UnitSectionOffset>,
|
||||
}
|
||||
|
||||
impl Dependencies {
|
||||
fn new() -> Dependencies {
|
||||
Dependencies {
|
||||
edges: HashMap::new(),
|
||||
roots: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_edge(&mut self, a: UnitSectionOffset, b: UnitSectionOffset) {
|
||||
use crate::hash_map::Entry;
|
||||
match self.edges.entry(a) {
|
||||
Entry::Occupied(mut o) => {
|
||||
o.get_mut().insert(b);
|
||||
}
|
||||
Entry::Vacant(v) => {
|
||||
let mut set = HashSet::new();
|
||||
set.insert(b);
|
||||
v.insert(set);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_root(&mut self, root: UnitSectionOffset) {
|
||||
self.roots.insert(root);
|
||||
}
|
||||
|
||||
pub fn get_reachable(&self) -> HashSet<UnitSectionOffset> {
|
||||
let mut reachable = self.roots.clone();
|
||||
let mut queue = Vec::new();
|
||||
for i in self.roots.iter() {
|
||||
if let Some(deps) = self.edges.get(i) {
|
||||
for j in deps {
|
||||
if reachable.contains(j) {
|
||||
continue;
|
||||
}
|
||||
reachable.insert(*j);
|
||||
queue.push(*j);
|
||||
}
|
||||
}
|
||||
}
|
||||
while let Some(i) = queue.pop() {
|
||||
if let Some(deps) = self.edges.get(&i) {
|
||||
for j in deps {
|
||||
if reachable.contains(j) {
|
||||
continue;
|
||||
}
|
||||
reachable.insert(*j);
|
||||
queue.push(*j);
|
||||
}
|
||||
}
|
||||
}
|
||||
reachable
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_dependencies<R: Reader<Offset = usize>>(
|
||||
dwarf: &read::Dwarf<R>,
|
||||
at: &AddressTransform,
|
||||
) -> read::Result<Dependencies> {
|
||||
let mut deps = Dependencies::new();
|
||||
let mut units = dwarf.units();
|
||||
while let Some(unit) = units.next()? {
|
||||
build_unit_dependencies(unit, dwarf, at, &mut deps)?;
|
||||
}
|
||||
Ok(deps)
|
||||
}
|
||||
|
||||
fn build_unit_dependencies<R: Reader<Offset = usize>>(
|
||||
header: read::CompilationUnitHeader<R>,
|
||||
dwarf: &read::Dwarf<R>,
|
||||
at: &AddressTransform,
|
||||
deps: &mut Dependencies,
|
||||
) -> read::Result<()> {
|
||||
let unit = dwarf.unit(header)?;
|
||||
let mut tree = unit.entries_tree(None)?;
|
||||
let root = tree.root()?;
|
||||
build_die_dependencies(root, dwarf, &unit, at, deps)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn has_die_back_edge<R: Reader<Offset = usize>>(die: &read::DebuggingInformationEntry<R>) -> bool {
|
||||
match die.tag() {
|
||||
constants::DW_TAG_variable
|
||||
| constants::DW_TAG_constant
|
||||
| constants::DW_TAG_inlined_subroutine
|
||||
| constants::DW_TAG_lexical_block
|
||||
| constants::DW_TAG_label
|
||||
| constants::DW_TAG_with_stmt
|
||||
| constants::DW_TAG_try_block
|
||||
| constants::DW_TAG_catch_block
|
||||
| constants::DW_TAG_template_type_parameter
|
||||
| constants::DW_TAG_member
|
||||
| constants::DW_TAG_formal_parameter => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn has_valid_code_range<R: Reader<Offset = usize>>(
|
||||
die: &read::DebuggingInformationEntry<R>,
|
||||
dwarf: &read::Dwarf<R>,
|
||||
unit: &read::Unit<R>,
|
||||
at: &AddressTransform,
|
||||
) -> read::Result<bool> {
|
||||
match die.tag() {
|
||||
constants::DW_TAG_subprogram => {
|
||||
if let Some(ranges_attr) = die.attr_value(constants::DW_AT_ranges)? {
|
||||
let offset = match ranges_attr {
|
||||
read::AttributeValue::RangeListsRef(val) => val,
|
||||
read::AttributeValue::DebugRngListsIndex(index) => {
|
||||
dwarf.ranges_offset(unit, index)?
|
||||
}
|
||||
_ => return Ok(false),
|
||||
};
|
||||
let mut has_valid_base = if let Some(read::AttributeValue::Addr(low_pc)) =
|
||||
die.attr_value(constants::DW_AT_low_pc)?
|
||||
{
|
||||
Some(at.can_translate_address(low_pc))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut it = dwarf.ranges.raw_ranges(offset, unit.encoding())?;
|
||||
while let Some(range) = it.next()? {
|
||||
// If at least one of the range addresses can be converted,
|
||||
// declaring code range as valid.
|
||||
match range {
|
||||
read::RawRngListEntry::AddressOrOffsetPair { .. }
|
||||
if has_valid_base.is_some() =>
|
||||
{
|
||||
if has_valid_base.unwrap() {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
read::RawRngListEntry::StartEnd { begin, .. }
|
||||
| read::RawRngListEntry::StartLength { begin, .. }
|
||||
| read::RawRngListEntry::AddressOrOffsetPair { begin, .. } => {
|
||||
if at.can_translate_address(begin) {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
read::RawRngListEntry::StartxEndx { begin, .. }
|
||||
| read::RawRngListEntry::StartxLength { begin, .. } => {
|
||||
let addr = dwarf.address(unit, begin)?;
|
||||
if at.can_translate_address(addr) {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
read::RawRngListEntry::BaseAddress { addr } => {
|
||||
has_valid_base = Some(at.can_translate_address(addr));
|
||||
}
|
||||
read::RawRngListEntry::BaseAddressx { addr } => {
|
||||
let addr = dwarf.address(unit, addr)?;
|
||||
has_valid_base = Some(at.can_translate_address(addr));
|
||||
}
|
||||
read::RawRngListEntry::OffsetPair { .. } => (),
|
||||
}
|
||||
}
|
||||
return Ok(false);
|
||||
} else if let Some(low_pc) = die.attr_value(constants::DW_AT_low_pc)? {
|
||||
if let read::AttributeValue::Addr(a) = low_pc {
|
||||
return Ok(at.can_translate_address(a));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn build_die_dependencies<R: Reader<Offset = usize>>(
|
||||
die: read::EntriesTreeNode<R>,
|
||||
dwarf: &read::Dwarf<R>,
|
||||
unit: &read::Unit<R>,
|
||||
at: &AddressTransform,
|
||||
deps: &mut Dependencies,
|
||||
) -> read::Result<()> {
|
||||
let entry = die.entry();
|
||||
let offset = entry.offset().to_unit_section_offset(unit);
|
||||
let mut attrs = entry.attrs();
|
||||
while let Some(attr) = attrs.next()? {
|
||||
build_attr_dependencies(&attr, offset, dwarf, unit, at, deps)?;
|
||||
}
|
||||
|
||||
let mut children = die.children();
|
||||
while let Some(child) = children.next()? {
|
||||
let child_entry = child.entry();
|
||||
let child_offset = child_entry.offset().to_unit_section_offset(unit);
|
||||
deps.add_edge(child_offset, offset);
|
||||
if has_die_back_edge(child_entry) {
|
||||
deps.add_edge(offset, child_offset);
|
||||
}
|
||||
if has_valid_code_range(child_entry, dwarf, unit, at)? {
|
||||
deps.add_root(child_offset);
|
||||
}
|
||||
build_die_dependencies(child, dwarf, unit, at, deps)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_attr_dependencies<R: Reader<Offset = usize>>(
|
||||
attr: &read::Attribute<R>,
|
||||
offset: UnitSectionOffset,
|
||||
_dwarf: &read::Dwarf<R>,
|
||||
unit: &read::Unit<R>,
|
||||
_at: &AddressTransform,
|
||||
deps: &mut Dependencies,
|
||||
) -> read::Result<()> {
|
||||
match attr.value() {
|
||||
read::AttributeValue::UnitRef(val) => {
|
||||
let ref_offset = val.to_unit_section_offset(unit);
|
||||
deps.add_edge(offset, ref_offset);
|
||||
}
|
||||
read::AttributeValue::DebugInfoRef(val) => {
|
||||
let ref_offset = UnitSectionOffset::DebugInfoOffset(val);
|
||||
deps.add_edge(offset, ref_offset);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
192
crates/debug/src/lib.rs
Normal file
192
crates/debug/src/lib.rs
Normal file
@@ -0,0 +1,192 @@
|
||||
//! Debug utils for WebAssembly using Cranelift.
|
||||
|
||||
#![allow(clippy::cast_ptr_alignment)]
|
||||
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use cranelift_codegen::isa::TargetFrontendConfig;
|
||||
use faerie::{Artifact, Decl};
|
||||
use failure::Error;
|
||||
use target_lexicon::{BinaryFormat, Triple};
|
||||
use wasmtime_environ::{ModuleAddressMap, ModuleVmctxInfo, ValueLabelsRanges};
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
use hashbrown::{hash_map, HashMap, HashSet};
|
||||
#[cfg(feature = "std")]
|
||||
use std::collections::{hash_map, HashMap, HashSet};
|
||||
|
||||
pub use crate::read_debuginfo::{read_debuginfo, DebugInfoData, WasmFileInfo};
|
||||
pub use crate::transform::transform_dwarf;
|
||||
pub use crate::write_debuginfo::{emit_dwarf, ResolvedSymbol, SymbolResolver};
|
||||
|
||||
mod gc;
|
||||
mod read_debuginfo;
|
||||
mod transform;
|
||||
mod write_debuginfo;
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
struct FunctionRelocResolver {}
|
||||
impl SymbolResolver for FunctionRelocResolver {
|
||||
fn resolve_symbol(&self, symbol: usize, addend: i64) -> ResolvedSymbol {
|
||||
let name = format!("_wasm_function_{}", symbol);
|
||||
ResolvedSymbol::Reloc { name, addend }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn emit_debugsections(
|
||||
obj: &mut Artifact,
|
||||
vmctx_info: &ModuleVmctxInfo,
|
||||
target_config: &TargetFrontendConfig,
|
||||
debuginfo_data: &DebugInfoData,
|
||||
at: &ModuleAddressMap,
|
||||
ranges: &ValueLabelsRanges,
|
||||
) -> Result<(), Error> {
|
||||
let resolver = FunctionRelocResolver {};
|
||||
let dwarf = transform_dwarf(target_config, debuginfo_data, at, vmctx_info, ranges)?;
|
||||
emit_dwarf(obj, dwarf, &resolver)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct ImageRelocResolver<'a> {
|
||||
func_offsets: &'a Vec<u64>,
|
||||
}
|
||||
|
||||
impl<'a> SymbolResolver for ImageRelocResolver<'a> {
|
||||
fn resolve_symbol(&self, symbol: usize, addend: i64) -> ResolvedSymbol {
|
||||
let func_start = self.func_offsets[symbol];
|
||||
ResolvedSymbol::PhysicalAddress(func_start + addend as u64)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn emit_debugsections_image(
|
||||
triple: Triple,
|
||||
target_config: &TargetFrontendConfig,
|
||||
debuginfo_data: &DebugInfoData,
|
||||
vmctx_info: &ModuleVmctxInfo,
|
||||
at: &ModuleAddressMap,
|
||||
ranges: &ValueLabelsRanges,
|
||||
funcs: &[(*const u8, usize)],
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
let func_offsets = &funcs
|
||||
.iter()
|
||||
.map(|(ptr, _)| *ptr as u64)
|
||||
.collect::<Vec<u64>>();
|
||||
let mut obj = Artifact::new(triple, String::from("module"));
|
||||
let resolver = ImageRelocResolver { func_offsets };
|
||||
let dwarf = transform_dwarf(target_config, debuginfo_data, at, vmctx_info, ranges)?;
|
||||
|
||||
// Assuming all functions in the same code block, looking min/max of its range.
|
||||
assert!(funcs.len() > 0);
|
||||
let mut segment_body: (usize, usize) = (!0, 0);
|
||||
for (body_ptr, body_len) in funcs {
|
||||
segment_body.0 = ::core::cmp::min(segment_body.0, *body_ptr as usize);
|
||||
segment_body.1 = ::core::cmp::max(segment_body.1, *body_ptr as usize + body_len);
|
||||
}
|
||||
let segment_body = (segment_body.0 as *const u8, segment_body.1 - segment_body.0);
|
||||
|
||||
let body = unsafe { ::core::slice::from_raw_parts(segment_body.0, segment_body.1) };
|
||||
obj.declare_with("all", Decl::function(), body.to_vec())?;
|
||||
|
||||
emit_dwarf(&mut obj, dwarf, &resolver)?;
|
||||
|
||||
// LLDB is too "magical" about mach-o, generating elf
|
||||
let mut bytes = obj.emit_as(BinaryFormat::Elf)?;
|
||||
// elf is still missing details...
|
||||
convert_faerie_elf_to_loadable_file(&mut bytes, segment_body.0);
|
||||
|
||||
// let mut file = ::std::fs::File::create(::std::path::Path::new("test.o")).expect("file");
|
||||
// ::std::io::Write::write(&mut file, &bytes).expect("write");
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
fn convert_faerie_elf_to_loadable_file(bytes: &mut Vec<u8>, code_ptr: *const u8) {
|
||||
use std::ffi::CStr;
|
||||
use std::os::raw::c_char;
|
||||
|
||||
assert!(
|
||||
bytes[0x4] == 2 && bytes[0x5] == 1,
|
||||
"bits and endianess in .ELF"
|
||||
);
|
||||
let e_phoff = unsafe { *(bytes.as_ptr().offset(0x20) as *const u64) };
|
||||
let e_phnum = unsafe { *(bytes.as_ptr().offset(0x38) as *const u16) };
|
||||
assert!(
|
||||
e_phoff == 0 && e_phnum == 0,
|
||||
"program header table is empty"
|
||||
);
|
||||
let e_phentsize = unsafe { *(bytes.as_ptr().offset(0x36) as *const u16) };
|
||||
assert!(e_phentsize == 0x38, "size of ph");
|
||||
let e_shentsize = unsafe { *(bytes.as_ptr().offset(0x3A) as *const u16) };
|
||||
assert!(e_shentsize == 0x40, "size of sh");
|
||||
|
||||
let e_shoff = unsafe { *(bytes.as_ptr().offset(0x28) as *const u64) };
|
||||
let e_shnum = unsafe { *(bytes.as_ptr().offset(0x3C) as *const u16) };
|
||||
let mut shstrtab_off = 0;
|
||||
let mut segment = None;
|
||||
for i in 0..e_shnum {
|
||||
let off = e_shoff as isize + i as isize * e_shentsize as isize;
|
||||
let sh_type = unsafe { *(bytes.as_ptr().offset(off + 0x4) as *const u32) };
|
||||
if sh_type == /* SHT_SYMTAB */ 3 {
|
||||
shstrtab_off = unsafe { *(bytes.as_ptr().offset(off + 0x18) as *const u64) };
|
||||
}
|
||||
if sh_type != /* SHT_PROGBITS */ 1 {
|
||||
continue;
|
||||
}
|
||||
// It is a SHT_PROGBITS, but we need to check sh_name to ensure it is our function
|
||||
let sh_name = unsafe {
|
||||
let sh_name_off = *(bytes.as_ptr().offset(off) as *const u32);
|
||||
CStr::from_ptr(
|
||||
bytes
|
||||
.as_ptr()
|
||||
.offset((shstrtab_off + sh_name_off as u64) as isize)
|
||||
as *const c_char,
|
||||
)
|
||||
.to_str()
|
||||
.expect("name")
|
||||
};
|
||||
if sh_name != ".text.all" {
|
||||
continue;
|
||||
}
|
||||
|
||||
assert!(segment.is_none());
|
||||
// Functions was added at emit_debugsections_image as .text.all.
|
||||
// Patch vaddr, and save file location and its size.
|
||||
unsafe {
|
||||
*(bytes.as_ptr().offset(off + 0x10) as *mut u64) = code_ptr as u64;
|
||||
};
|
||||
let sh_offset = unsafe { *(bytes.as_ptr().offset(off + 0x18) as *const u64) };
|
||||
let sh_size = unsafe { *(bytes.as_ptr().offset(off + 0x20) as *const u64) };
|
||||
segment = Some((sh_offset, code_ptr, sh_size));
|
||||
// Fix name too: cut it to just ".text"
|
||||
unsafe {
|
||||
let sh_name_off = *(bytes.as_ptr().offset(off) as *const u32);
|
||||
bytes[(shstrtab_off + sh_name_off as u64) as usize + ".text".len()] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// LLDB wants segment with virtual address set, placing them at the end of ELF.
|
||||
let ph_off = bytes.len();
|
||||
if let Some((sh_offset, v_offset, sh_size)) = segment {
|
||||
let segment = vec![0; 0x38];
|
||||
unsafe {
|
||||
*(segment.as_ptr() as *mut u32) = /* PT_LOAD */ 0x1;
|
||||
*(segment.as_ptr().offset(0x8) as *mut u64) = sh_offset;
|
||||
*(segment.as_ptr().offset(0x10) as *mut u64) = v_offset as u64;
|
||||
*(segment.as_ptr().offset(0x18) as *mut u64) = v_offset as u64;
|
||||
*(segment.as_ptr().offset(0x20) as *mut u64) = sh_size;
|
||||
*(segment.as_ptr().offset(0x28) as *mut u64) = sh_size;
|
||||
}
|
||||
bytes.extend_from_slice(&segment);
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
// It is somewhat loadable ELF file at this moment.
|
||||
// Update e_flags, e_phoff and e_phnum.
|
||||
unsafe {
|
||||
*(bytes.as_ptr().offset(0x10) as *mut u16) = /* ET_DYN */ 3;
|
||||
*(bytes.as_ptr().offset(0x20) as *mut u64) = ph_off as u64;
|
||||
*(bytes.as_ptr().offset(0x38) as *mut u16) = 1 as u16;
|
||||
}
|
||||
}
|
||||
251
crates/debug/src/read_debuginfo.rs
Normal file
251
crates/debug/src/read_debuginfo.rs
Normal file
@@ -0,0 +1,251 @@
|
||||
use alloc::boxed::Box;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use wasmparser::{self, ModuleReader, SectionCode};
|
||||
|
||||
use gimli;
|
||||
|
||||
use gimli::{
|
||||
DebugAbbrev, DebugAddr, DebugInfo, DebugLine, DebugLineStr, DebugLoc, DebugLocLists,
|
||||
DebugRanges, DebugRngLists, DebugStr, DebugStrOffsets, DebugTypes, EndianSlice, LittleEndian,
|
||||
LocationLists, RangeLists,
|
||||
};
|
||||
|
||||
trait Reader: gimli::Reader<Offset = usize, Endian = LittleEndian> {}
|
||||
|
||||
impl<'input> Reader for gimli::EndianSlice<'input, LittleEndian> {}
|
||||
|
||||
pub use wasmparser::Type as WasmType;
|
||||
|
||||
pub type Dwarf<'input> = gimli::Dwarf<gimli::EndianSlice<'input, LittleEndian>>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FunctionMetadata {
|
||||
pub params: Box<[WasmType]>,
|
||||
pub locals: Box<[(u32, WasmType)]>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WasmFileInfo {
|
||||
pub path: Option<PathBuf>,
|
||||
pub code_section_offset: u64,
|
||||
pub funcs: Box<[FunctionMetadata]>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NameSection {
|
||||
pub module_name: Option<String>,
|
||||
pub func_names: HashMap<u32, String>,
|
||||
pub locals_names: HashMap<u32, HashMap<u32, String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DebugInfoData<'a> {
|
||||
pub dwarf: Dwarf<'a>,
|
||||
pub name_section: Option<NameSection>,
|
||||
pub wasm_file: WasmFileInfo,
|
||||
}
|
||||
|
||||
fn convert_sections<'a>(sections: HashMap<&str, &'a [u8]>) -> Dwarf<'a> {
|
||||
const EMPTY_SECTION: &[u8] = &[];
|
||||
|
||||
let endian = LittleEndian;
|
||||
let debug_str = DebugStr::new(sections.get(".debug_str").unwrap_or(&EMPTY_SECTION), endian);
|
||||
let debug_abbrev = DebugAbbrev::new(
|
||||
sections.get(".debug_abbrev").unwrap_or(&EMPTY_SECTION),
|
||||
endian,
|
||||
);
|
||||
let debug_info = DebugInfo::new(
|
||||
sections.get(".debug_info").unwrap_or(&EMPTY_SECTION),
|
||||
endian,
|
||||
);
|
||||
let debug_line = DebugLine::new(
|
||||
sections.get(".debug_line").unwrap_or(&EMPTY_SECTION),
|
||||
endian,
|
||||
);
|
||||
|
||||
if sections.contains_key(".debug_addr") {
|
||||
panic!("Unexpected .debug_addr");
|
||||
}
|
||||
|
||||
let debug_addr = DebugAddr::from(EndianSlice::new(EMPTY_SECTION, endian));
|
||||
|
||||
if sections.contains_key(".debug_line_str") {
|
||||
panic!("Unexpected .debug_line_str");
|
||||
}
|
||||
|
||||
let debug_line_str = DebugLineStr::from(EndianSlice::new(EMPTY_SECTION, endian));
|
||||
let debug_str_sup = DebugStr::from(EndianSlice::new(EMPTY_SECTION, endian));
|
||||
|
||||
if sections.contains_key(".debug_rnglists") {
|
||||
panic!("Unexpected .debug_rnglists");
|
||||
}
|
||||
|
||||
let debug_ranges = match sections.get(".debug_ranges") {
|
||||
Some(section) => DebugRanges::new(section, endian),
|
||||
None => DebugRanges::new(EMPTY_SECTION, endian),
|
||||
};
|
||||
let debug_rnglists = DebugRngLists::new(EMPTY_SECTION, endian);
|
||||
let ranges = RangeLists::new(debug_ranges, debug_rnglists);
|
||||
|
||||
if sections.contains_key(".debug_loclists") {
|
||||
panic!("Unexpected .debug_loclists");
|
||||
}
|
||||
|
||||
let debug_loc = match sections.get(".debug_loc") {
|
||||
Some(section) => DebugLoc::new(section, endian),
|
||||
None => DebugLoc::new(EMPTY_SECTION, endian),
|
||||
};
|
||||
let debug_loclists = DebugLocLists::new(EMPTY_SECTION, endian);
|
||||
let locations = LocationLists::new(debug_loc, debug_loclists);
|
||||
|
||||
if sections.contains_key(".debug_str_offsets") {
|
||||
panic!("Unexpected .debug_str_offsets");
|
||||
}
|
||||
|
||||
let debug_str_offsets = DebugStrOffsets::from(EndianSlice::new(EMPTY_SECTION, endian));
|
||||
|
||||
if sections.contains_key(".debug_types") {
|
||||
panic!("Unexpected .debug_types");
|
||||
}
|
||||
|
||||
let debug_types = DebugTypes::from(EndianSlice::new(EMPTY_SECTION, endian));
|
||||
|
||||
Dwarf {
|
||||
debug_abbrev,
|
||||
debug_addr,
|
||||
debug_info,
|
||||
debug_line,
|
||||
debug_line_str,
|
||||
debug_str,
|
||||
debug_str_offsets,
|
||||
debug_str_sup,
|
||||
debug_types,
|
||||
locations,
|
||||
ranges,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_name_section(reader: wasmparser::NameSectionReader) -> wasmparser::Result<NameSection> {
|
||||
let mut module_name = None;
|
||||
let mut func_names = HashMap::new();
|
||||
let mut locals_names = HashMap::new();
|
||||
for i in reader.into_iter() {
|
||||
match i? {
|
||||
wasmparser::Name::Module(m) => {
|
||||
module_name = Some(String::from(m.get_name()?));
|
||||
}
|
||||
wasmparser::Name::Function(f) => {
|
||||
let mut reader = f.get_map()?;
|
||||
while let Ok(naming) = reader.read() {
|
||||
func_names.insert(naming.index, String::from(naming.name));
|
||||
}
|
||||
}
|
||||
wasmparser::Name::Local(l) => {
|
||||
let mut reader = l.get_function_local_reader()?;
|
||||
while let Ok(f) = reader.read() {
|
||||
let mut names = HashMap::new();
|
||||
let mut reader = f.get_map()?;
|
||||
while let Ok(naming) = reader.read() {
|
||||
names.insert(naming.index, String::from(naming.name));
|
||||
}
|
||||
locals_names.insert(f.func_index, names);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let result = NameSection {
|
||||
module_name,
|
||||
func_names,
|
||||
locals_names,
|
||||
};
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn read_debuginfo(data: &[u8]) -> DebugInfoData {
|
||||
let mut reader = ModuleReader::new(data).expect("reader");
|
||||
let mut sections = HashMap::new();
|
||||
let mut name_section = None;
|
||||
let mut code_section_offset = 0;
|
||||
|
||||
let mut signatures_params: Vec<Box<[WasmType]>> = Vec::new();
|
||||
let mut func_params_refs: Vec<usize> = Vec::new();
|
||||
let mut func_locals: Vec<Box<[(u32, WasmType)]>> = Vec::new();
|
||||
|
||||
while !reader.eof() {
|
||||
let section = reader.read().expect("section");
|
||||
match section.code {
|
||||
SectionCode::Custom { name, .. } => {
|
||||
if name.starts_with(".debug_") {
|
||||
let mut reader = section.get_binary_reader();
|
||||
let len = reader.bytes_remaining();
|
||||
sections.insert(name, reader.read_bytes(len).expect("bytes"));
|
||||
}
|
||||
if name == "name" {
|
||||
if let Ok(reader) = section.get_name_section_reader() {
|
||||
if let Ok(section) = read_name_section(reader) {
|
||||
name_section = Some(section);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionCode::Type => {
|
||||
signatures_params = section
|
||||
.get_type_section_reader()
|
||||
.expect("type section")
|
||||
.into_iter()
|
||||
.map(|ft| ft.expect("type").params)
|
||||
.collect::<Vec<_>>();
|
||||
}
|
||||
SectionCode::Function => {
|
||||
func_params_refs = section
|
||||
.get_function_section_reader()
|
||||
.expect("function section")
|
||||
.into_iter()
|
||||
.map(|index| index.expect("func index") as usize)
|
||||
.collect::<Vec<_>>();
|
||||
}
|
||||
SectionCode::Code => {
|
||||
code_section_offset = section.range().start as u64;
|
||||
func_locals = section
|
||||
.get_code_section_reader()
|
||||
.expect("code section")
|
||||
.into_iter()
|
||||
.map(|body| {
|
||||
let locals = body
|
||||
.expect("body")
|
||||
.get_locals_reader()
|
||||
.expect("locals reader");
|
||||
locals
|
||||
.into_iter()
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.expect("locals data")
|
||||
.into_boxed_slice()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
let func_meta = func_params_refs
|
||||
.into_iter()
|
||||
.zip(func_locals.into_iter())
|
||||
.map(|(params_index, locals)| FunctionMetadata {
|
||||
params: signatures_params[params_index].clone(),
|
||||
locals,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
DebugInfoData {
|
||||
dwarf: convert_sections(sections),
|
||||
name_section,
|
||||
wasm_file: WasmFileInfo {
|
||||
path: None,
|
||||
code_section_offset,
|
||||
funcs: func_meta.into_boxed_slice(),
|
||||
},
|
||||
}
|
||||
}
|
||||
658
crates/debug/src/transform/address_transform.rs
Normal file
658
crates/debug/src/transform/address_transform.rs
Normal file
@@ -0,0 +1,658 @@
|
||||
use crate::HashMap;
|
||||
use crate::WasmFileInfo;
|
||||
use alloc::boxed::Box;
|
||||
use alloc::collections::BTreeMap;
|
||||
use alloc::vec::Vec;
|
||||
use core::iter::FromIterator;
|
||||
use cranelift_codegen::ir::SourceLoc;
|
||||
use cranelift_entity::{EntityRef, PrimaryMap};
|
||||
use cranelift_wasm::DefinedFuncIndex;
|
||||
use gimli::write;
|
||||
use wasmtime_environ::{FunctionAddressMap, ModuleAddressMap};
|
||||
|
||||
pub type GeneratedAddress = usize;
|
||||
pub type WasmAddress = u64;
|
||||
|
||||
/// Contains mapping of the generated address to its original
|
||||
/// source location.
|
||||
#[derive(Debug)]
|
||||
pub struct AddressMap {
|
||||
pub generated: GeneratedAddress,
|
||||
pub wasm: WasmAddress,
|
||||
}
|
||||
|
||||
/// Information about generated function code: its body start,
|
||||
/// length, and instructions addresses.
|
||||
#[derive(Debug)]
|
||||
pub struct FunctionMap {
|
||||
pub offset: GeneratedAddress,
|
||||
pub len: GeneratedAddress,
|
||||
pub wasm_start: WasmAddress,
|
||||
pub wasm_end: WasmAddress,
|
||||
pub addresses: Box<[AddressMap]>,
|
||||
}
|
||||
|
||||
/// Mapping of the source location to its generated code range.
|
||||
#[derive(Debug)]
|
||||
struct Position {
|
||||
wasm_pos: WasmAddress,
|
||||
gen_start: GeneratedAddress,
|
||||
gen_end: GeneratedAddress,
|
||||
}
|
||||
|
||||
/// Mapping of continuous range of source location to its generated
|
||||
/// code. The positions are always in accending order for search.
|
||||
#[derive(Debug)]
|
||||
struct Range {
|
||||
wasm_start: WasmAddress,
|
||||
wasm_end: WasmAddress,
|
||||
gen_start: GeneratedAddress,
|
||||
gen_end: GeneratedAddress,
|
||||
positions: Box<[Position]>,
|
||||
}
|
||||
|
||||
/// Helper function address lookup data. Contains ranges start positions
|
||||
/// index and ranges data. The multiple ranges can include the same
|
||||
/// original source position. The index (B-Tree) uses range start
|
||||
/// position as a key.
|
||||
#[derive(Debug)]
|
||||
struct FuncLookup {
|
||||
index: Vec<(WasmAddress, Box<[usize]>)>,
|
||||
ranges: Box<[Range]>,
|
||||
}
|
||||
|
||||
/// Mapping of original functions to generated code locations/ranges.
|
||||
#[derive(Debug)]
|
||||
struct FuncTransform {
|
||||
start: WasmAddress,
|
||||
end: WasmAddress,
|
||||
index: DefinedFuncIndex,
|
||||
lookup: FuncLookup,
|
||||
}
|
||||
|
||||
/// Module functions mapping to generated code.
|
||||
#[derive(Debug)]
|
||||
pub struct AddressTransform {
|
||||
map: PrimaryMap<DefinedFuncIndex, FunctionMap>,
|
||||
func: Vec<(WasmAddress, FuncTransform)>,
|
||||
}
|
||||
|
||||
/// Returns a wasm bytecode offset in the code section from SourceLoc.
|
||||
pub fn get_wasm_code_offset(loc: SourceLoc, code_section_offset: u64) -> WasmAddress {
|
||||
// Code section size <= 4GB, allow wrapped SourceLoc to recover the overflow.
|
||||
loc.bits().wrapping_sub(code_section_offset as u32) as WasmAddress
|
||||
}
|
||||
|
||||
fn build_function_lookup(
|
||||
ft: &FunctionAddressMap,
|
||||
code_section_offset: u64,
|
||||
) -> (WasmAddress, WasmAddress, FuncLookup) {
|
||||
assert!(code_section_offset <= ft.start_srcloc.bits() as u64);
|
||||
let fn_start = get_wasm_code_offset(ft.start_srcloc, code_section_offset);
|
||||
let fn_end = get_wasm_code_offset(ft.end_srcloc, code_section_offset);
|
||||
assert!(fn_start <= fn_end);
|
||||
|
||||
// Build ranges of continuous source locations. The new ranges starts when
|
||||
// non-descending order is interrupted. Assuming the same origin location can
|
||||
// be present in multiple ranges.
|
||||
let mut range_wasm_start = fn_start;
|
||||
let mut range_gen_start = ft.body_offset;
|
||||
let mut last_wasm_pos = range_wasm_start;
|
||||
let mut ranges = Vec::new();
|
||||
let mut ranges_index = BTreeMap::new();
|
||||
let mut current_range = Vec::new();
|
||||
for t in &ft.instructions {
|
||||
if t.srcloc.is_default() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let offset = get_wasm_code_offset(t.srcloc, code_section_offset);
|
||||
assert!(fn_start <= offset && offset <= fn_end);
|
||||
|
||||
let inst_gen_start = t.code_offset;
|
||||
let inst_gen_end = t.code_offset + t.code_len;
|
||||
|
||||
if last_wasm_pos > offset {
|
||||
// Start new range.
|
||||
ranges_index.insert(range_wasm_start, ranges.len());
|
||||
ranges.push(Range {
|
||||
wasm_start: range_wasm_start,
|
||||
wasm_end: last_wasm_pos,
|
||||
gen_start: range_gen_start,
|
||||
gen_end: inst_gen_start,
|
||||
positions: current_range.into_boxed_slice(),
|
||||
});
|
||||
range_wasm_start = offset;
|
||||
range_gen_start = inst_gen_start;
|
||||
current_range = Vec::new();
|
||||
}
|
||||
// Continue existing range: add new wasm->generated code position.
|
||||
current_range.push(Position {
|
||||
wasm_pos: offset,
|
||||
gen_start: inst_gen_start,
|
||||
gen_end: inst_gen_end,
|
||||
});
|
||||
last_wasm_pos = offset;
|
||||
}
|
||||
let last_gen_addr = ft.body_offset + ft.body_len;
|
||||
ranges_index.insert(range_wasm_start, ranges.len());
|
||||
ranges.push(Range {
|
||||
wasm_start: range_wasm_start,
|
||||
wasm_end: fn_end,
|
||||
gen_start: range_gen_start,
|
||||
gen_end: last_gen_addr,
|
||||
positions: current_range.into_boxed_slice(),
|
||||
});
|
||||
|
||||
// Making ranges lookup faster by building index: B-tree with every range
|
||||
// start position that maps into list of active ranges at this position.
|
||||
let ranges = ranges.into_boxed_slice();
|
||||
let mut active_ranges = Vec::new();
|
||||
let mut index = BTreeMap::new();
|
||||
let mut last_wasm_pos = None;
|
||||
for (wasm_start, range_index) in ranges_index {
|
||||
if Some(wasm_start) == last_wasm_pos {
|
||||
active_ranges.push(range_index);
|
||||
continue;
|
||||
}
|
||||
if last_wasm_pos.is_some() {
|
||||
index.insert(
|
||||
last_wasm_pos.unwrap(),
|
||||
active_ranges.clone().into_boxed_slice(),
|
||||
);
|
||||
}
|
||||
active_ranges.retain(|r| ranges[*r].wasm_end.cmp(&wasm_start) != core::cmp::Ordering::Less);
|
||||
active_ranges.push(range_index);
|
||||
last_wasm_pos = Some(wasm_start);
|
||||
}
|
||||
index.insert(last_wasm_pos.unwrap(), active_ranges.into_boxed_slice());
|
||||
let index = Vec::from_iter(index.into_iter());
|
||||
(fn_start, fn_end, FuncLookup { index, ranges })
|
||||
}
|
||||
|
||||
fn build_function_addr_map(
|
||||
at: &ModuleAddressMap,
|
||||
code_section_offset: u64,
|
||||
) -> PrimaryMap<DefinedFuncIndex, FunctionMap> {
|
||||
let mut map = PrimaryMap::new();
|
||||
for (_, ft) in at {
|
||||
let mut fn_map = Vec::new();
|
||||
for t in &ft.instructions {
|
||||
if t.srcloc.is_default() {
|
||||
continue;
|
||||
}
|
||||
let offset = get_wasm_code_offset(t.srcloc, code_section_offset);
|
||||
fn_map.push(AddressMap {
|
||||
generated: t.code_offset,
|
||||
wasm: offset,
|
||||
});
|
||||
}
|
||||
|
||||
if cfg!(debug) {
|
||||
// fn_map is sorted by the generated field -- see FunctionAddressMap::instructions.
|
||||
for i in 1..fn_map.len() {
|
||||
assert!(fn_map[i - 1].generated <= fn_map[i].generated);
|
||||
}
|
||||
}
|
||||
|
||||
map.push(FunctionMap {
|
||||
offset: ft.body_offset,
|
||||
len: ft.body_len,
|
||||
wasm_start: get_wasm_code_offset(ft.start_srcloc, code_section_offset),
|
||||
wasm_end: get_wasm_code_offset(ft.end_srcloc, code_section_offset),
|
||||
addresses: fn_map.into_boxed_slice(),
|
||||
});
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
struct TransformRangeIter<'a> {
|
||||
addr: u64,
|
||||
indicies: &'a [usize],
|
||||
ranges: &'a [Range],
|
||||
}
|
||||
|
||||
impl<'a> TransformRangeIter<'a> {
|
||||
fn new(func: &'a FuncTransform, addr: u64) -> Self {
|
||||
let found = match func
|
||||
.lookup
|
||||
.index
|
||||
.binary_search_by(|entry| entry.0.cmp(&addr))
|
||||
{
|
||||
Ok(i) => Some(&func.lookup.index[i].1),
|
||||
Err(i) => {
|
||||
if i > 0 {
|
||||
Some(&func.lookup.index[i - 1].1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
if let Some(range_indices) = found {
|
||||
TransformRangeIter {
|
||||
addr,
|
||||
indicies: range_indices,
|
||||
ranges: &func.lookup.ranges,
|
||||
}
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'a> Iterator for TransformRangeIter<'a> {
|
||||
type Item = (usize, usize);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Some((first, tail)) = self.indicies.split_first() {
|
||||
let range_index = *first;
|
||||
let range = &self.ranges[range_index];
|
||||
self.indicies = tail;
|
||||
let address = match range
|
||||
.positions
|
||||
.binary_search_by(|a| a.wasm_pos.cmp(&self.addr))
|
||||
{
|
||||
Ok(i) => range.positions[i].gen_start,
|
||||
Err(i) => {
|
||||
if i == 0 {
|
||||
range.gen_start
|
||||
} else {
|
||||
range.positions[i - 1].gen_end
|
||||
}
|
||||
}
|
||||
};
|
||||
Some((address, range_index))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TransformRangeEndIter<'a> {
|
||||
addr: u64,
|
||||
indicies: &'a [usize],
|
||||
ranges: &'a [Range],
|
||||
}
|
||||
|
||||
impl<'a> TransformRangeEndIter<'a> {
|
||||
fn new(func: &'a FuncTransform, addr: u64) -> Self {
|
||||
let found = match func
|
||||
.lookup
|
||||
.index
|
||||
.binary_search_by(|entry| entry.0.cmp(&addr))
|
||||
{
|
||||
Ok(i) => Some(&func.lookup.index[i].1),
|
||||
Err(i) => {
|
||||
if i > 0 {
|
||||
Some(&func.lookup.index[i - 1].1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
if let Some(range_indices) = found {
|
||||
TransformRangeEndIter {
|
||||
addr,
|
||||
indicies: range_indices,
|
||||
ranges: &func.lookup.ranges,
|
||||
}
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for TransformRangeEndIter<'a> {
|
||||
type Item = (usize, usize);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
while let Some((first, tail)) = self.indicies.split_first() {
|
||||
let range_index = *first;
|
||||
let range = &self.ranges[range_index];
|
||||
if range.wasm_start >= self.addr {
|
||||
continue;
|
||||
}
|
||||
self.indicies = tail;
|
||||
let address = match range
|
||||
.positions
|
||||
.binary_search_by(|a| a.wasm_pos.cmp(&self.addr))
|
||||
{
|
||||
Ok(i) => range.positions[i].gen_end,
|
||||
Err(i) => {
|
||||
if i == range.positions.len() {
|
||||
range.gen_end
|
||||
} else {
|
||||
range.positions[i].gen_start
|
||||
}
|
||||
}
|
||||
};
|
||||
return Some((address, range_index));
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl AddressTransform {
|
||||
pub fn new(at: &ModuleAddressMap, wasm_file: &WasmFileInfo) -> Self {
|
||||
let code_section_offset = wasm_file.code_section_offset;
|
||||
|
||||
let mut func = BTreeMap::new();
|
||||
for (i, ft) in at {
|
||||
let (fn_start, fn_end, lookup) = build_function_lookup(ft, code_section_offset);
|
||||
|
||||
func.insert(
|
||||
fn_start,
|
||||
FuncTransform {
|
||||
start: fn_start,
|
||||
end: fn_end,
|
||||
index: i,
|
||||
lookup,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let map = build_function_addr_map(at, code_section_offset);
|
||||
let func = Vec::from_iter(func.into_iter());
|
||||
AddressTransform { map, func }
|
||||
}
|
||||
|
||||
fn find_func(&self, addr: u64) -> Option<&FuncTransform> {
|
||||
// TODO check if we need to include end address
|
||||
let func = match self.func.binary_search_by(|entry| entry.0.cmp(&addr)) {
|
||||
Ok(i) => &self.func[i].1,
|
||||
Err(i) => {
|
||||
if i > 0 {
|
||||
&self.func[i - 1].1
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
};
|
||||
if addr >= func.start {
|
||||
return Some(func);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn find_func_index(&self, addr: u64) -> Option<DefinedFuncIndex> {
|
||||
self.find_func(addr).map(|f| f.index)
|
||||
}
|
||||
|
||||
pub fn translate_raw(&self, addr: u64) -> Option<(DefinedFuncIndex, GeneratedAddress)> {
|
||||
if addr == 0 {
|
||||
// It's normally 0 for debug info without the linked code.
|
||||
return None;
|
||||
}
|
||||
if let Some(func) = self.find_func(addr) {
|
||||
if addr == func.end {
|
||||
// Clamp last address to the end to extend translation to the end
|
||||
// of the function.
|
||||
let map = &self.map[func.index];
|
||||
return Some((func.index, map.len));
|
||||
}
|
||||
let first_result = TransformRangeIter::new(func, addr).next();
|
||||
first_result.map(|(address, _)| (func.index, address))
|
||||
} else {
|
||||
// Address was not found: function was not compiled?
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_translate_address(&self, addr: u64) -> bool {
|
||||
self.translate(addr).is_some()
|
||||
}
|
||||
|
||||
pub fn translate(&self, addr: u64) -> Option<write::Address> {
|
||||
self.translate_raw(addr)
|
||||
.map(|(func_index, address)| write::Address::Symbol {
|
||||
symbol: func_index.index(),
|
||||
addend: address as i64,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn translate_ranges_raw(
|
||||
&self,
|
||||
start: u64,
|
||||
end: u64,
|
||||
) -> Option<(DefinedFuncIndex, Vec<(GeneratedAddress, GeneratedAddress)>)> {
|
||||
if start == 0 {
|
||||
// It's normally 0 for debug info without the linked code.
|
||||
return None;
|
||||
}
|
||||
if let Some(func) = self.find_func(start) {
|
||||
let mut starts: HashMap<usize, usize> =
|
||||
HashMap::from_iter(TransformRangeIter::new(func, start).map(|(a, r)| (r, a)));
|
||||
let mut result = Vec::new();
|
||||
TransformRangeEndIter::new(func, end).for_each(|(a, r)| {
|
||||
let range_start = if let Some(range_start) = starts.get(&r) {
|
||||
let range_start = *range_start;
|
||||
starts.remove(&r);
|
||||
range_start
|
||||
} else {
|
||||
let range = &func.lookup.ranges[r];
|
||||
range.gen_start
|
||||
};
|
||||
result.push((range_start, a));
|
||||
});
|
||||
for (r, range_start) in starts {
|
||||
let range = &func.lookup.ranges[r];
|
||||
result.push((range_start, range.gen_end));
|
||||
}
|
||||
return Some((func.index, result));
|
||||
}
|
||||
// Address was not found: function was not compiled?
|
||||
None
|
||||
}
|
||||
|
||||
pub fn translate_ranges(&self, start: u64, end: u64) -> Vec<(write::Address, u64)> {
|
||||
self.translate_ranges_raw(start, end)
|
||||
.map_or(vec![], |(func_index, ranges)| {
|
||||
ranges
|
||||
.iter()
|
||||
.map(|(start, end)| {
|
||||
(
|
||||
write::Address::Symbol {
|
||||
symbol: func_index.index(),
|
||||
addend: *start as i64,
|
||||
},
|
||||
(*end - *start) as u64,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn map(&self) -> &PrimaryMap<DefinedFuncIndex, FunctionMap> {
|
||||
&self.map
|
||||
}
|
||||
|
||||
pub fn func_range(&self, index: DefinedFuncIndex) -> (GeneratedAddress, GeneratedAddress) {
|
||||
let map = &self.map[index];
|
||||
(map.offset, map.offset + map.len)
|
||||
}
|
||||
|
||||
pub fn func_source_range(&self, index: DefinedFuncIndex) -> (WasmAddress, WasmAddress) {
|
||||
let map = &self.map[index];
|
||||
(map.wasm_start, map.wasm_end)
|
||||
}
|
||||
|
||||
pub fn convert_to_code_range(
|
||||
&self,
|
||||
addr: write::Address,
|
||||
len: u64,
|
||||
) -> (GeneratedAddress, GeneratedAddress) {
|
||||
let start = if let write::Address::Symbol { addend, .. } = addr {
|
||||
// TODO subtract self.map[symbol].offset ?
|
||||
addend as GeneratedAddress
|
||||
} else {
|
||||
unreachable!();
|
||||
};
|
||||
(start, start + len as GeneratedAddress)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{build_function_lookup, get_wasm_code_offset, AddressTransform};
|
||||
use crate::read_debuginfo::WasmFileInfo;
|
||||
use core::iter::FromIterator;
|
||||
use cranelift_codegen::ir::SourceLoc;
|
||||
use cranelift_entity::PrimaryMap;
|
||||
use gimli::write::Address;
|
||||
use wasmtime_environ::{FunctionAddressMap, InstructionAddressMap, ModuleAddressMap};
|
||||
|
||||
#[test]
|
||||
fn test_get_wasm_code_offset() {
|
||||
let offset = get_wasm_code_offset(SourceLoc::new(3), 1);
|
||||
assert_eq!(2, offset);
|
||||
let offset = get_wasm_code_offset(SourceLoc::new(16), 0xF000_0000);
|
||||
assert_eq!(0x1000_0010, offset);
|
||||
let offset = get_wasm_code_offset(SourceLoc::new(1), 0x20_8000_0000);
|
||||
assert_eq!(0x8000_0001, offset);
|
||||
}
|
||||
|
||||
fn create_simple_func(wasm_offset: u32) -> FunctionAddressMap {
|
||||
FunctionAddressMap {
|
||||
instructions: vec![
|
||||
InstructionAddressMap {
|
||||
srcloc: SourceLoc::new(wasm_offset + 2),
|
||||
code_offset: 5,
|
||||
code_len: 3,
|
||||
},
|
||||
InstructionAddressMap {
|
||||
srcloc: SourceLoc::new(wasm_offset + 7),
|
||||
code_offset: 15,
|
||||
code_len: 8,
|
||||
},
|
||||
],
|
||||
start_srcloc: SourceLoc::new(wasm_offset),
|
||||
end_srcloc: SourceLoc::new(wasm_offset + 10),
|
||||
body_offset: 0,
|
||||
body_len: 30,
|
||||
}
|
||||
}
|
||||
|
||||
fn create_simple_module(func: FunctionAddressMap) -> ModuleAddressMap {
|
||||
PrimaryMap::from_iter(vec![func])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_function_lookup_simple() {
|
||||
let input = create_simple_func(11);
|
||||
let (start, end, lookup) = build_function_lookup(&input, 1);
|
||||
assert_eq!(10, start);
|
||||
assert_eq!(20, end);
|
||||
|
||||
assert_eq!(1, lookup.index.len());
|
||||
let index_entry = lookup.index.into_iter().next().unwrap();
|
||||
assert_eq!((10u64, vec![0].into_boxed_slice()), index_entry);
|
||||
assert_eq!(1, lookup.ranges.len());
|
||||
let range = &lookup.ranges[0];
|
||||
assert_eq!(10, range.wasm_start);
|
||||
assert_eq!(20, range.wasm_end);
|
||||
assert_eq!(0, range.gen_start);
|
||||
assert_eq!(30, range.gen_end);
|
||||
let positions = &range.positions;
|
||||
assert_eq!(2, positions.len());
|
||||
assert_eq!(12, positions[0].wasm_pos);
|
||||
assert_eq!(5, positions[0].gen_start);
|
||||
assert_eq!(8, positions[0].gen_end);
|
||||
assert_eq!(17, positions[1].wasm_pos);
|
||||
assert_eq!(15, positions[1].gen_start);
|
||||
assert_eq!(23, positions[1].gen_end);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_function_lookup_two_ranges() {
|
||||
let mut input = create_simple_func(11);
|
||||
// append instruction with same srcloc as input.instructions[0]
|
||||
input.instructions.push(InstructionAddressMap {
|
||||
srcloc: SourceLoc::new(11 + 2),
|
||||
code_offset: 23,
|
||||
code_len: 3,
|
||||
});
|
||||
let (start, end, lookup) = build_function_lookup(&input, 1);
|
||||
assert_eq!(10, start);
|
||||
assert_eq!(20, end);
|
||||
|
||||
assert_eq!(2, lookup.index.len());
|
||||
let index_entries = Vec::from_iter(lookup.index.into_iter());
|
||||
assert_eq!((10u64, vec![0].into_boxed_slice()), index_entries[0]);
|
||||
assert_eq!((12u64, vec![0, 1].into_boxed_slice()), index_entries[1]);
|
||||
assert_eq!(2, lookup.ranges.len());
|
||||
|
||||
let range = &lookup.ranges[0];
|
||||
assert_eq!(10, range.wasm_start);
|
||||
assert_eq!(17, range.wasm_end);
|
||||
assert_eq!(0, range.gen_start);
|
||||
assert_eq!(23, range.gen_end);
|
||||
let positions = &range.positions;
|
||||
assert_eq!(2, positions.len());
|
||||
assert_eq!(12, positions[0].wasm_pos);
|
||||
assert_eq!(5, positions[0].gen_start);
|
||||
assert_eq!(8, positions[0].gen_end);
|
||||
assert_eq!(17, positions[1].wasm_pos);
|
||||
assert_eq!(15, positions[1].gen_start);
|
||||
assert_eq!(23, positions[1].gen_end);
|
||||
|
||||
let range = &lookup.ranges[1];
|
||||
assert_eq!(12, range.wasm_start);
|
||||
assert_eq!(20, range.wasm_end);
|
||||
assert_eq!(23, range.gen_start);
|
||||
assert_eq!(30, range.gen_end);
|
||||
let positions = &range.positions;
|
||||
assert_eq!(1, positions.len());
|
||||
assert_eq!(12, positions[0].wasm_pos);
|
||||
assert_eq!(23, positions[0].gen_start);
|
||||
assert_eq!(26, positions[0].gen_end);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_addr_translate() {
|
||||
let input = create_simple_module(create_simple_func(11));
|
||||
let at = AddressTransform::new(
|
||||
&input,
|
||||
&WasmFileInfo {
|
||||
path: None,
|
||||
code_section_offset: 1,
|
||||
funcs: Box::new([]),
|
||||
},
|
||||
);
|
||||
|
||||
let addr = at.translate(10);
|
||||
assert_eq!(
|
||||
Some(Address::Symbol {
|
||||
symbol: 0,
|
||||
addend: 0,
|
||||
}),
|
||||
addr
|
||||
);
|
||||
|
||||
let addr = at.translate(20);
|
||||
assert_eq!(
|
||||
Some(Address::Symbol {
|
||||
symbol: 0,
|
||||
addend: 30,
|
||||
}),
|
||||
addr
|
||||
);
|
||||
|
||||
let addr = at.translate(0);
|
||||
assert_eq!(None, addr);
|
||||
|
||||
let addr = at.translate(12);
|
||||
assert_eq!(
|
||||
Some(Address::Symbol {
|
||||
symbol: 0,
|
||||
addend: 5,
|
||||
}),
|
||||
addr
|
||||
);
|
||||
|
||||
let addr = at.translate(18);
|
||||
assert_eq!(
|
||||
Some(Address::Symbol {
|
||||
symbol: 0,
|
||||
addend: 23,
|
||||
}),
|
||||
addr
|
||||
);
|
||||
}
|
||||
}
|
||||
294
crates/debug/src/transform/attr.rs
Normal file
294
crates/debug/src/transform/attr.rs
Normal file
@@ -0,0 +1,294 @@
|
||||
use crate::HashMap;
|
||||
use alloc::vec::Vec;
|
||||
use failure::Error;
|
||||
|
||||
use gimli;
|
||||
|
||||
use gimli::{AttributeValue, DebugLineOffset, DebugStr, DebuggingInformationEntry, UnitOffset};
|
||||
|
||||
use gimli::write;
|
||||
|
||||
use super::address_transform::AddressTransform;
|
||||
use super::expression::{compile_expression, CompiledExpression, FunctionFrameInfo};
|
||||
use super::range_info_builder::RangeInfoBuilder;
|
||||
use super::unit::PendingDieRef;
|
||||
use super::{DebugInputContext, Reader, TransformError};
|
||||
|
||||
pub(crate) enum FileAttributeContext<'a> {
|
||||
Root(Option<DebugLineOffset>),
|
||||
Children(&'a Vec<write::FileId>, Option<&'a CompiledExpression>),
|
||||
}
|
||||
|
||||
fn is_exprloc_to_loclist_allowed(attr_name: gimli::constants::DwAt) -> bool {
|
||||
match attr_name {
|
||||
gimli::DW_AT_location
|
||||
| gimli::DW_AT_string_length
|
||||
| gimli::DW_AT_return_addr
|
||||
| gimli::DW_AT_data_member_location
|
||||
| gimli::DW_AT_frame_base
|
||||
| gimli::DW_AT_segment
|
||||
| gimli::DW_AT_static_link
|
||||
| gimli::DW_AT_use_location
|
||||
| gimli::DW_AT_vtable_elem_location => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clone_die_attributes<'a, R>(
|
||||
entry: &DebuggingInformationEntry<R>,
|
||||
context: &DebugInputContext<R>,
|
||||
addr_tr: &'a AddressTransform,
|
||||
frame_info: Option<&FunctionFrameInfo>,
|
||||
unit_encoding: gimli::Encoding,
|
||||
out_unit: &mut write::Unit,
|
||||
current_scope_id: write::UnitEntryId,
|
||||
subprogram_range_builder: Option<RangeInfoBuilder>,
|
||||
scope_ranges: Option<&Vec<(u64, u64)>>,
|
||||
cu_low_pc: u64,
|
||||
out_strings: &mut write::StringTable,
|
||||
die_ref_map: &HashMap<UnitOffset, write::UnitEntryId>,
|
||||
pending_die_refs: &mut Vec<PendingDieRef>,
|
||||
file_context: FileAttributeContext<'a>,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
R: Reader,
|
||||
{
|
||||
let _tag = &entry.tag();
|
||||
let endian = gimli::RunTimeEndian::Little;
|
||||
|
||||
let range_info = if let Some(subprogram_range_builder) = subprogram_range_builder {
|
||||
subprogram_range_builder
|
||||
} else if entry.tag() == gimli::DW_TAG_compile_unit {
|
||||
// FIXME currently address_transform operate on a single func range,
|
||||
// once it is fixed we can properly set DW_AT_ranges attribute.
|
||||
// Using for now DW_AT_low_pc = 0.
|
||||
RangeInfoBuilder::Position(0)
|
||||
} else {
|
||||
RangeInfoBuilder::from(entry, context, unit_encoding, cu_low_pc)?
|
||||
};
|
||||
range_info.build(addr_tr, out_unit, current_scope_id);
|
||||
|
||||
let mut attrs = entry.attrs();
|
||||
while let Some(attr) = attrs.next()? {
|
||||
let attr_value = match attr.value() {
|
||||
AttributeValue::Addr(_) if attr.name() == gimli::DW_AT_low_pc => {
|
||||
continue;
|
||||
}
|
||||
AttributeValue::Udata(_) if attr.name() == gimli::DW_AT_high_pc => {
|
||||
continue;
|
||||
}
|
||||
AttributeValue::RangeListsRef(_) if attr.name() == gimli::DW_AT_ranges => {
|
||||
continue;
|
||||
}
|
||||
AttributeValue::Exprloc(_) if attr.name() == gimli::DW_AT_frame_base => {
|
||||
continue;
|
||||
}
|
||||
|
||||
AttributeValue::Addr(u) => {
|
||||
let addr = addr_tr.translate(u).unwrap_or(write::Address::Constant(0));
|
||||
write::AttributeValue::Address(addr)
|
||||
}
|
||||
AttributeValue::Udata(u) => write::AttributeValue::Udata(u),
|
||||
AttributeValue::Data1(d) => write::AttributeValue::Data1(d),
|
||||
AttributeValue::Data2(d) => write::AttributeValue::Data2(d),
|
||||
AttributeValue::Data4(d) => write::AttributeValue::Data4(d),
|
||||
AttributeValue::Sdata(d) => write::AttributeValue::Sdata(d),
|
||||
AttributeValue::Flag(f) => write::AttributeValue::Flag(f),
|
||||
AttributeValue::DebugLineRef(line_program_offset) => {
|
||||
if let FileAttributeContext::Root(o) = file_context {
|
||||
if o != Some(line_program_offset) {
|
||||
return Err(TransformError("invalid debug_line offset").into());
|
||||
}
|
||||
write::AttributeValue::LineProgramRef
|
||||
} else {
|
||||
return Err(TransformError("unexpected debug_line index attribute").into());
|
||||
}
|
||||
}
|
||||
AttributeValue::FileIndex(i) => {
|
||||
if let FileAttributeContext::Children(file_map, _) = file_context {
|
||||
write::AttributeValue::FileIndex(Some(file_map[(i - 1) as usize]))
|
||||
} else {
|
||||
return Err(TransformError("unexpected file index attribute").into());
|
||||
}
|
||||
}
|
||||
AttributeValue::DebugStrRef(str_offset) => {
|
||||
let s = context.debug_str.get_str(str_offset)?.to_slice()?.to_vec();
|
||||
write::AttributeValue::StringRef(out_strings.add(s))
|
||||
}
|
||||
AttributeValue::RangeListsRef(r) => {
|
||||
let range_info =
|
||||
RangeInfoBuilder::from_ranges_ref(r, context, unit_encoding, cu_low_pc)?;
|
||||
let range_list_id = range_info.build_ranges(addr_tr, &mut out_unit.ranges);
|
||||
write::AttributeValue::RangeListRef(range_list_id)
|
||||
}
|
||||
AttributeValue::LocationListsRef(r) => {
|
||||
let low_pc = 0;
|
||||
let mut locs = context.loclists.locations(
|
||||
r,
|
||||
unit_encoding,
|
||||
low_pc,
|
||||
&context.debug_addr,
|
||||
context.debug_addr_base,
|
||||
)?;
|
||||
let frame_base = if let FileAttributeContext::Children(_, frame_base) = file_context
|
||||
{
|
||||
frame_base
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut result = None;
|
||||
while let Some(loc) = locs.next()? {
|
||||
if let Some(expr) = compile_expression(&loc.data, unit_encoding, frame_base)? {
|
||||
if result.is_none() {
|
||||
result = Some(Vec::new());
|
||||
}
|
||||
for (start, len, expr) in expr.build_with_locals(
|
||||
&[(loc.range.begin, loc.range.end)],
|
||||
addr_tr,
|
||||
frame_info,
|
||||
endian,
|
||||
) {
|
||||
if len == 0 {
|
||||
// Ignore empty range
|
||||
continue;
|
||||
}
|
||||
result.as_mut().unwrap().push(write::Location::StartLength {
|
||||
begin: start,
|
||||
length: len,
|
||||
data: expr,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// FIXME _expr contains invalid expression
|
||||
continue; // ignore entry
|
||||
}
|
||||
}
|
||||
if result.is_none() {
|
||||
continue; // no valid locations
|
||||
}
|
||||
let list_id = out_unit.locations.add(write::LocationList(result.unwrap()));
|
||||
write::AttributeValue::LocationListRef(list_id)
|
||||
}
|
||||
AttributeValue::Exprloc(ref expr) => {
|
||||
let frame_base = if let FileAttributeContext::Children(_, frame_base) = file_context
|
||||
{
|
||||
frame_base
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(expr) = compile_expression(expr, unit_encoding, frame_base)? {
|
||||
if expr.is_simple() {
|
||||
if let Some(expr) = expr.build() {
|
||||
write::AttributeValue::Exprloc(expr)
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Conversion to loclist is required.
|
||||
if let Some(scope_ranges) = scope_ranges {
|
||||
let exprs =
|
||||
expr.build_with_locals(scope_ranges, addr_tr, frame_info, endian);
|
||||
if exprs.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let found_single_expr = {
|
||||
// Micro-optimization all expressions alike, use one exprloc.
|
||||
let mut found_expr: Option<write::Expression> = None;
|
||||
for (_, _, expr) in &exprs {
|
||||
if let Some(ref prev_expr) = found_expr {
|
||||
if expr.0.eq(&prev_expr.0) {
|
||||
continue; // the same expression
|
||||
}
|
||||
found_expr = None;
|
||||
break;
|
||||
}
|
||||
found_expr = Some(expr.clone())
|
||||
}
|
||||
found_expr
|
||||
};
|
||||
if found_single_expr.is_some() {
|
||||
write::AttributeValue::Exprloc(found_single_expr.unwrap())
|
||||
} else if is_exprloc_to_loclist_allowed(attr.name()) {
|
||||
// Converting exprloc to loclist.
|
||||
let mut locs = Vec::new();
|
||||
for (begin, length, data) in exprs {
|
||||
if length == 0 {
|
||||
// Ignore empty range
|
||||
continue;
|
||||
}
|
||||
locs.push(write::Location::StartLength {
|
||||
begin,
|
||||
length,
|
||||
data,
|
||||
});
|
||||
}
|
||||
let list_id = out_unit.locations.add(write::LocationList(locs));
|
||||
write::AttributeValue::LocationListRef(list_id)
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// FIXME _expr contains invalid expression
|
||||
continue; // ignore attribute
|
||||
}
|
||||
}
|
||||
AttributeValue::Encoding(e) => write::AttributeValue::Encoding(e),
|
||||
AttributeValue::DecimalSign(e) => write::AttributeValue::DecimalSign(e),
|
||||
AttributeValue::Endianity(e) => write::AttributeValue::Endianity(e),
|
||||
AttributeValue::Accessibility(e) => write::AttributeValue::Accessibility(e),
|
||||
AttributeValue::Visibility(e) => write::AttributeValue::Visibility(e),
|
||||
AttributeValue::Virtuality(e) => write::AttributeValue::Virtuality(e),
|
||||
AttributeValue::Language(e) => write::AttributeValue::Language(e),
|
||||
AttributeValue::AddressClass(e) => write::AttributeValue::AddressClass(e),
|
||||
AttributeValue::IdentifierCase(e) => write::AttributeValue::IdentifierCase(e),
|
||||
AttributeValue::CallingConvention(e) => write::AttributeValue::CallingConvention(e),
|
||||
AttributeValue::Inline(e) => write::AttributeValue::Inline(e),
|
||||
AttributeValue::Ordering(e) => write::AttributeValue::Ordering(e),
|
||||
AttributeValue::UnitRef(ref offset) => {
|
||||
if let Some(unit_id) = die_ref_map.get(offset) {
|
||||
write::AttributeValue::ThisUnitEntryRef(*unit_id)
|
||||
} else {
|
||||
pending_die_refs.push((current_scope_id, attr.name(), *offset));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// AttributeValue::DebugInfoRef(_) => {
|
||||
// continue;
|
||||
// }
|
||||
_ => panic!(), //write::AttributeValue::StringRef(out_strings.add("_")),
|
||||
};
|
||||
let current_scope = out_unit.get_mut(current_scope_id);
|
||||
current_scope.set(attr.name(), attr_value);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn clone_attr_string<R>(
|
||||
attr_value: &AttributeValue<R>,
|
||||
form: gimli::DwForm,
|
||||
debug_str: &DebugStr<R>,
|
||||
out_strings: &mut write::StringTable,
|
||||
) -> Result<write::LineString, gimli::Error>
|
||||
where
|
||||
R: Reader,
|
||||
{
|
||||
let content = match attr_value {
|
||||
AttributeValue::DebugStrRef(str_offset) => {
|
||||
debug_str.get_str(*str_offset)?.to_slice()?.to_vec()
|
||||
}
|
||||
AttributeValue::String(b) => b.to_slice()?.to_vec(),
|
||||
_ => panic!("Unexpected attribute value"),
|
||||
};
|
||||
Ok(match form {
|
||||
gimli::DW_FORM_strp => {
|
||||
let id = out_strings.add(content);
|
||||
write::LineString::StringRef(id)
|
||||
}
|
||||
gimli::DW_FORM_string => write::LineString::String(content),
|
||||
_ => panic!("DW_FORM_line_strp or other not supported"),
|
||||
})
|
||||
}
|
||||
491
crates/debug/src/transform/expression.rs
Normal file
491
crates/debug/src/transform/expression.rs
Normal file
@@ -0,0 +1,491 @@
|
||||
use crate::{HashMap, HashSet};
|
||||
use alloc::vec::Vec;
|
||||
use cranelift_codegen::ir::{StackSlots, ValueLabel, ValueLoc};
|
||||
use cranelift_codegen::isa::RegUnit;
|
||||
use cranelift_codegen::ValueLabelsRanges;
|
||||
use cranelift_entity::EntityRef;
|
||||
use cranelift_wasm::{get_vmctx_value_label, DefinedFuncIndex};
|
||||
use failure::Error;
|
||||
use gimli::write;
|
||||
use gimli::{self, Expression, Operation, Reader, ReaderOffset, Register, X86_64};
|
||||
|
||||
use super::address_transform::AddressTransform;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FunctionFrameInfo<'a> {
|
||||
pub value_ranges: &'a ValueLabelsRanges,
|
||||
pub memory_offset: i64,
|
||||
pub stack_slots: &'a StackSlots,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum CompiledExpressionPart {
|
||||
Code(Vec<u8>),
|
||||
Local(ValueLabel),
|
||||
Deref,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CompiledExpression {
|
||||
parts: Vec<CompiledExpressionPart>,
|
||||
need_deref: bool,
|
||||
}
|
||||
|
||||
impl Clone for CompiledExpressionPart {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
CompiledExpressionPart::Code(c) => CompiledExpressionPart::Code(c.clone()),
|
||||
CompiledExpressionPart::Local(i) => CompiledExpressionPart::Local(*i),
|
||||
CompiledExpressionPart::Deref => CompiledExpressionPart::Deref,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CompiledExpression {
|
||||
pub fn vmctx() -> CompiledExpression {
|
||||
CompiledExpression::from_label(get_vmctx_value_label())
|
||||
}
|
||||
|
||||
pub fn from_label(label: ValueLabel) -> CompiledExpression {
|
||||
CompiledExpression {
|
||||
parts: vec![
|
||||
CompiledExpressionPart::Local(label),
|
||||
CompiledExpressionPart::Code(vec![gimli::constants::DW_OP_stack_value.0 as u8]),
|
||||
],
|
||||
need_deref: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn map_reg(reg: RegUnit) -> Register {
|
||||
static mut REG_X86_MAP: Option<HashMap<RegUnit, Register>> = None;
|
||||
// FIXME lazy initialization?
|
||||
unsafe {
|
||||
if REG_X86_MAP.is_none() {
|
||||
REG_X86_MAP = Some(HashMap::new());
|
||||
}
|
||||
if let Some(val) = REG_X86_MAP.as_mut().unwrap().get(®) {
|
||||
return *val;
|
||||
}
|
||||
let result = match reg {
|
||||
0 => X86_64::RAX,
|
||||
1 => X86_64::RCX,
|
||||
2 => X86_64::RDX,
|
||||
3 => X86_64::RBX,
|
||||
4 => X86_64::RSP,
|
||||
5 => X86_64::RBP,
|
||||
6 => X86_64::RSI,
|
||||
7 => X86_64::RDI,
|
||||
8 => X86_64::R8,
|
||||
9 => X86_64::R9,
|
||||
10 => X86_64::R10,
|
||||
11 => X86_64::R11,
|
||||
12 => X86_64::R12,
|
||||
13 => X86_64::R13,
|
||||
14 => X86_64::R14,
|
||||
15 => X86_64::R15,
|
||||
16 => X86_64::XMM0,
|
||||
17 => X86_64::XMM1,
|
||||
18 => X86_64::XMM2,
|
||||
19 => X86_64::XMM3,
|
||||
20 => X86_64::XMM4,
|
||||
21 => X86_64::XMM5,
|
||||
22 => X86_64::XMM6,
|
||||
23 => X86_64::XMM7,
|
||||
_ => panic!("{}", reg),
|
||||
};
|
||||
REG_X86_MAP.as_mut().unwrap().insert(reg, result);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
fn translate_loc(loc: ValueLoc, frame_info: Option<&FunctionFrameInfo>) -> Option<Vec<u8>> {
|
||||
match loc {
|
||||
ValueLoc::Reg(reg) => {
|
||||
let machine_reg = map_reg(reg).0 as u8;
|
||||
assert!(machine_reg < 32); // FIXME
|
||||
Some(vec![gimli::constants::DW_OP_reg0.0 + machine_reg])
|
||||
}
|
||||
ValueLoc::Stack(ss) => {
|
||||
if let Some(frame_info) = frame_info {
|
||||
if let Some(ss_offset) = frame_info.stack_slots[ss].offset {
|
||||
use gimli::write::Writer;
|
||||
let endian = gimli::RunTimeEndian::Little;
|
||||
let mut writer = write::EndianVec::new(endian);
|
||||
writer
|
||||
.write_u8(gimli::constants::DW_OP_breg0.0 + X86_64::RBP.0 as u8)
|
||||
.expect("bp wr");
|
||||
writer.write_sleb128(ss_offset as i64 + 16).expect("ss wr");
|
||||
writer
|
||||
.write_u8(gimli::constants::DW_OP_deref.0 as u8)
|
||||
.expect("bp wr");
|
||||
let buf = writer.into_vec();
|
||||
return Some(buf);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn append_memory_deref(
|
||||
buf: &mut Vec<u8>,
|
||||
frame_info: &FunctionFrameInfo,
|
||||
vmctx_loc: ValueLoc,
|
||||
endian: gimli::RunTimeEndian,
|
||||
) -> write::Result<bool> {
|
||||
use gimli::write::Writer;
|
||||
let mut writer = write::EndianVec::new(endian);
|
||||
match vmctx_loc {
|
||||
ValueLoc::Reg(vmctx_reg) => {
|
||||
let reg = map_reg(vmctx_reg);
|
||||
writer.write_u8(gimli::constants::DW_OP_breg0.0 + reg.0 as u8)?;
|
||||
writer.write_sleb128(frame_info.memory_offset)?;
|
||||
}
|
||||
ValueLoc::Stack(ss) => {
|
||||
if let Some(ss_offset) = frame_info.stack_slots[ss].offset {
|
||||
writer.write_u8(gimli::constants::DW_OP_breg0.0 + X86_64::RBP.0 as u8)?;
|
||||
writer.write_sleb128(ss_offset as i64 + 16)?;
|
||||
writer.write_u8(gimli::constants::DW_OP_deref.0 as u8)?;
|
||||
|
||||
writer.write_u8(gimli::constants::DW_OP_consts.0 as u8)?;
|
||||
writer.write_sleb128(frame_info.memory_offset)?;
|
||||
writer.write_u8(gimli::constants::DW_OP_plus.0 as u8)?;
|
||||
} else {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
writer.write_u8(gimli::constants::DW_OP_deref.0 as u8)?;
|
||||
writer.write_u8(gimli::constants::DW_OP_swap.0 as u8)?;
|
||||
writer.write_u8(gimli::constants::DW_OP_stack_value.0 as u8)?;
|
||||
writer.write_u8(gimli::constants::DW_OP_constu.0 as u8)?;
|
||||
writer.write_uleb128(0xffff_ffff)?;
|
||||
writer.write_u8(gimli::constants::DW_OP_and.0 as u8)?;
|
||||
writer.write_u8(gimli::constants::DW_OP_plus.0 as u8)?;
|
||||
buf.extend_from_slice(writer.slice());
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
impl CompiledExpression {
|
||||
pub fn is_simple(&self) -> bool {
|
||||
if let [CompiledExpressionPart::Code(_)] = self.parts.as_slice() {
|
||||
true
|
||||
} else {
|
||||
self.parts.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build(&self) -> Option<write::Expression> {
|
||||
if let [CompiledExpressionPart::Code(code)] = self.parts.as_slice() {
|
||||
return Some(write::Expression(code.to_vec()));
|
||||
}
|
||||
// locals found, not supported
|
||||
None
|
||||
}
|
||||
|
||||
pub fn build_with_locals(
|
||||
&self,
|
||||
scope: &[(u64, u64)], // wasm ranges
|
||||
addr_tr: &AddressTransform,
|
||||
frame_info: Option<&FunctionFrameInfo>,
|
||||
endian: gimli::RunTimeEndian,
|
||||
) -> alloc::vec::Vec<(write::Address, u64, write::Expression)> {
|
||||
if scope.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
if let [CompiledExpressionPart::Code(code)] = self.parts.as_slice() {
|
||||
let mut result_scope = Vec::new();
|
||||
for s in scope {
|
||||
for (addr, len) in addr_tr.translate_ranges(s.0, s.1) {
|
||||
result_scope.push((addr, len, write::Expression(code.to_vec())));
|
||||
}
|
||||
}
|
||||
return result_scope;
|
||||
}
|
||||
|
||||
let vmctx_label = get_vmctx_value_label();
|
||||
|
||||
// Some locals are present, preparing and divided ranges based on the scope
|
||||
// and frame_info data.
|
||||
let mut ranges_builder = ValueLabelRangesBuilder::new(scope, addr_tr, frame_info);
|
||||
for p in &self.parts {
|
||||
match p {
|
||||
CompiledExpressionPart::Code(_) => (),
|
||||
CompiledExpressionPart::Local(label) => ranges_builder.process_label(*label),
|
||||
CompiledExpressionPart::Deref => ranges_builder.process_label(vmctx_label),
|
||||
}
|
||||
}
|
||||
if self.need_deref {
|
||||
ranges_builder.process_label(vmctx_label);
|
||||
}
|
||||
ranges_builder.remove_incomplete_ranges();
|
||||
let ranges = ranges_builder.ranges;
|
||||
|
||||
let mut result = Vec::new();
|
||||
'range: for CachedValueLabelRange {
|
||||
func_index,
|
||||
start,
|
||||
end,
|
||||
label_location,
|
||||
} in ranges
|
||||
{
|
||||
// build expression
|
||||
let mut code_buf = Vec::new();
|
||||
for part in &self.parts {
|
||||
match part {
|
||||
CompiledExpressionPart::Code(c) => code_buf.extend_from_slice(c.as_slice()),
|
||||
CompiledExpressionPart::Local(label) => {
|
||||
let loc = *label_location.get(&label).expect("loc");
|
||||
if let Some(expr) = translate_loc(loc, frame_info) {
|
||||
code_buf.extend_from_slice(&expr)
|
||||
} else {
|
||||
continue 'range;
|
||||
}
|
||||
}
|
||||
CompiledExpressionPart::Deref => {
|
||||
if let (Some(vmctx_loc), Some(frame_info)) =
|
||||
(label_location.get(&vmctx_label), frame_info)
|
||||
{
|
||||
if !append_memory_deref(&mut code_buf, frame_info, *vmctx_loc, endian)
|
||||
.expect("append_memory_deref")
|
||||
{
|
||||
continue 'range;
|
||||
}
|
||||
} else {
|
||||
continue 'range;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.need_deref {
|
||||
if let (Some(vmctx_loc), Some(frame_info)) =
|
||||
(label_location.get(&vmctx_label), frame_info)
|
||||
{
|
||||
if !append_memory_deref(&mut code_buf, frame_info, *vmctx_loc, endian)
|
||||
.expect("append_memory_deref")
|
||||
{
|
||||
continue 'range;
|
||||
}
|
||||
} else {
|
||||
continue 'range;
|
||||
};
|
||||
}
|
||||
result.push((
|
||||
write::Address::Symbol {
|
||||
symbol: func_index.index(),
|
||||
addend: start as i64,
|
||||
},
|
||||
(end - start) as u64,
|
||||
write::Expression(code_buf),
|
||||
));
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compile_expression<R>(
|
||||
expr: &Expression<R>,
|
||||
encoding: gimli::Encoding,
|
||||
frame_base: Option<&CompiledExpression>,
|
||||
) -> Result<Option<CompiledExpression>, Error>
|
||||
where
|
||||
R: Reader,
|
||||
{
|
||||
let mut parts = Vec::new();
|
||||
let mut need_deref = false;
|
||||
if let Some(frame_base) = frame_base {
|
||||
parts.extend_from_slice(&frame_base.parts);
|
||||
need_deref = frame_base.need_deref;
|
||||
}
|
||||
let base_len = parts.len();
|
||||
let mut pc = expr.0.clone();
|
||||
let mut code_chunk = Vec::new();
|
||||
let buf = expr.0.to_slice()?;
|
||||
while !pc.is_empty() {
|
||||
let next = buf[pc.offset_from(&expr.0).into_u64() as usize];
|
||||
need_deref = true;
|
||||
if next == 0xED {
|
||||
// WebAssembly DWARF extension
|
||||
pc.read_u8()?;
|
||||
let ty = pc.read_uleb128()?;
|
||||
assert_eq!(ty, 0);
|
||||
let index = pc.read_sleb128()?;
|
||||
pc.read_u8()?; // consume 159
|
||||
if code_chunk.len() > 0 {
|
||||
parts.push(CompiledExpressionPart::Code(code_chunk));
|
||||
code_chunk = Vec::new();
|
||||
}
|
||||
let label = ValueLabel::from_u32(index as u32);
|
||||
parts.push(CompiledExpressionPart::Local(label));
|
||||
} else {
|
||||
let pos = pc.offset_from(&expr.0).into_u64() as usize;
|
||||
let op = Operation::parse(&mut pc, &expr.0, encoding)?;
|
||||
match op {
|
||||
Operation::Literal { .. } | Operation::PlusConstant { .. } => (),
|
||||
Operation::StackValue => {
|
||||
need_deref = false;
|
||||
}
|
||||
Operation::Deref { .. } => {
|
||||
if code_chunk.len() > 0 {
|
||||
parts.push(CompiledExpressionPart::Code(code_chunk));
|
||||
code_chunk = Vec::new();
|
||||
}
|
||||
parts.push(CompiledExpressionPart::Deref);
|
||||
}
|
||||
_ => {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
let chunk = &buf[pos..pc.offset_from(&expr.0).into_u64() as usize];
|
||||
code_chunk.extend_from_slice(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
if code_chunk.len() > 0 {
|
||||
parts.push(CompiledExpressionPart::Code(code_chunk));
|
||||
}
|
||||
|
||||
if base_len > 0 && base_len + 1 < parts.len() {
|
||||
// see if we can glue two code chunks
|
||||
if let [CompiledExpressionPart::Code(cc1), CompiledExpressionPart::Code(cc2)] =
|
||||
&parts[base_len..base_len + 1]
|
||||
{
|
||||
let mut combined = cc1.clone();
|
||||
combined.extend_from_slice(cc2);
|
||||
parts[base_len] = CompiledExpressionPart::Code(combined);
|
||||
parts.remove(base_len + 1);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Some(CompiledExpression { parts, need_deref }))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct CachedValueLabelRange {
|
||||
func_index: DefinedFuncIndex,
|
||||
start: usize,
|
||||
end: usize,
|
||||
label_location: HashMap<ValueLabel, ValueLoc>,
|
||||
}
|
||||
|
||||
struct ValueLabelRangesBuilder<'a, 'b> {
|
||||
ranges: Vec<CachedValueLabelRange>,
|
||||
addr_tr: &'a AddressTransform,
|
||||
frame_info: Option<&'a FunctionFrameInfo<'b>>,
|
||||
processed_labels: HashSet<ValueLabel>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> ValueLabelRangesBuilder<'a, 'b> {
|
||||
fn new(
|
||||
scope: &[(u64, u64)], // wasm ranges
|
||||
addr_tr: &'a AddressTransform,
|
||||
frame_info: Option<&'a FunctionFrameInfo<'b>>,
|
||||
) -> Self {
|
||||
let mut ranges = Vec::new();
|
||||
for s in scope {
|
||||
if let Some((func_index, tr)) = addr_tr.translate_ranges_raw(s.0, s.1) {
|
||||
for (start, end) in tr {
|
||||
ranges.push(CachedValueLabelRange {
|
||||
func_index,
|
||||
start,
|
||||
end,
|
||||
label_location: HashMap::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
ranges.sort_unstable_by(|a, b| a.start.cmp(&b.start));
|
||||
ValueLabelRangesBuilder {
|
||||
ranges,
|
||||
addr_tr,
|
||||
frame_info,
|
||||
processed_labels: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn process_label(&mut self, label: ValueLabel) {
|
||||
if self.processed_labels.contains(&label) {
|
||||
return;
|
||||
}
|
||||
self.processed_labels.insert(label);
|
||||
|
||||
let value_ranges = if let Some(frame_info) = self.frame_info {
|
||||
&frame_info.value_ranges
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let ranges = &mut self.ranges;
|
||||
if let Some(local_ranges) = value_ranges.get(&label) {
|
||||
for local_range in local_ranges {
|
||||
let wasm_start = local_range.start;
|
||||
let wasm_end = local_range.end;
|
||||
let loc = local_range.loc;
|
||||
// Find all native ranges for the value label ranges.
|
||||
for (addr, len) in self
|
||||
.addr_tr
|
||||
.translate_ranges(wasm_start as u64, wasm_end as u64)
|
||||
{
|
||||
let (range_start, range_end) = self.addr_tr.convert_to_code_range(addr, len);
|
||||
if range_start == range_end {
|
||||
continue;
|
||||
}
|
||||
assert!(range_start < range_end);
|
||||
// Find acceptable scope of ranges to intersect with.
|
||||
let i = match ranges.binary_search_by(|s| s.start.cmp(&range_start)) {
|
||||
Ok(i) => i,
|
||||
Err(i) => {
|
||||
if i > 0 && range_start < ranges[i - 1].end {
|
||||
i - 1
|
||||
} else {
|
||||
i
|
||||
}
|
||||
}
|
||||
};
|
||||
let j = match ranges.binary_search_by(|s| s.start.cmp(&range_end)) {
|
||||
Ok(i) | Err(i) => i,
|
||||
};
|
||||
// Starting for the end, intersect (range_start..range_end) with
|
||||
// self.ranges array.
|
||||
for i in (i..j).rev() {
|
||||
if range_end <= ranges[i].start || ranges[i].end <= range_start {
|
||||
continue;
|
||||
}
|
||||
if range_end < ranges[i].end {
|
||||
// Cutting some of the range from the end.
|
||||
let mut tail = ranges[i].clone();
|
||||
ranges[i].end = range_end;
|
||||
tail.start = range_end;
|
||||
ranges.insert(i + 1, tail);
|
||||
}
|
||||
assert!(ranges[i].end <= range_end);
|
||||
if range_start <= ranges[i].start {
|
||||
ranges[i].label_location.insert(label, loc);
|
||||
continue;
|
||||
}
|
||||
// Cutting some of the range from the start.
|
||||
let mut tail = ranges[i].clone();
|
||||
ranges[i].end = range_start;
|
||||
tail.start = range_start;
|
||||
tail.label_location.insert(label, loc);
|
||||
ranges.insert(i + 1, tail);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_incomplete_ranges(&mut self) {
|
||||
// Ranges with not-enough labels are discarded.
|
||||
let processed_labels_len = self.processed_labels.len();
|
||||
self.ranges
|
||||
.retain(|r| r.label_location.len() == processed_labels_len);
|
||||
}
|
||||
}
|
||||
234
crates/debug/src/transform/line_program.rs
Normal file
234
crates/debug/src/transform/line_program.rs
Normal file
@@ -0,0 +1,234 @@
|
||||
use alloc::collections::BTreeMap;
|
||||
use alloc::vec::Vec;
|
||||
use core::iter::FromIterator;
|
||||
use cranelift_entity::EntityRef;
|
||||
use failure::Error;
|
||||
|
||||
use gimli;
|
||||
|
||||
use gimli::{DebugLine, DebugLineOffset, DebugStr, DebuggingInformationEntry, LineEncoding, Unit};
|
||||
|
||||
use gimli::write;
|
||||
|
||||
use super::address_transform::AddressTransform;
|
||||
use super::attr::clone_attr_string;
|
||||
use super::{Reader, TransformError};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum SavedLineProgramRow {
|
||||
Normal {
|
||||
address: u64,
|
||||
op_index: u64,
|
||||
file_index: u64,
|
||||
line: u64,
|
||||
column: u64,
|
||||
discriminator: u64,
|
||||
is_stmt: bool,
|
||||
basic_block: bool,
|
||||
prologue_end: bool,
|
||||
epilogue_begin: bool,
|
||||
isa: u64,
|
||||
},
|
||||
EndOfSequence(u64),
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
enum ReadLineProgramState {
|
||||
SequenceEnded,
|
||||
ReadSequence,
|
||||
IgnoreSequence,
|
||||
}
|
||||
|
||||
pub(crate) fn clone_line_program<R>(
|
||||
unit: &Unit<R, R::Offset>,
|
||||
root: &DebuggingInformationEntry<R>,
|
||||
addr_tr: &AddressTransform,
|
||||
out_encoding: gimli::Encoding,
|
||||
debug_str: &DebugStr<R>,
|
||||
debug_line: &DebugLine<R>,
|
||||
out_strings: &mut write::StringTable,
|
||||
) -> Result<(write::LineProgram, DebugLineOffset, Vec<write::FileId>), Error>
|
||||
where
|
||||
R: Reader,
|
||||
{
|
||||
let offset = match root.attr_value(gimli::DW_AT_stmt_list)? {
|
||||
Some(gimli::AttributeValue::DebugLineRef(offset)) => offset,
|
||||
_ => {
|
||||
return Err(TransformError("Debug line offset is not found").into());
|
||||
}
|
||||
};
|
||||
let comp_dir = root.attr_value(gimli::DW_AT_comp_dir)?;
|
||||
let comp_name = root.attr_value(gimli::DW_AT_name)?;
|
||||
let out_comp_dir = clone_attr_string(
|
||||
comp_dir.as_ref().expect("comp_dir"),
|
||||
gimli::DW_FORM_strp,
|
||||
debug_str,
|
||||
out_strings,
|
||||
)?;
|
||||
let out_comp_name = clone_attr_string(
|
||||
comp_name.as_ref().expect("comp_name"),
|
||||
gimli::DW_FORM_strp,
|
||||
debug_str,
|
||||
out_strings,
|
||||
)?;
|
||||
|
||||
let program = debug_line.program(
|
||||
offset,
|
||||
unit.header.address_size(),
|
||||
comp_dir.and_then(|val| val.string_value(&debug_str)),
|
||||
comp_name.and_then(|val| val.string_value(&debug_str)),
|
||||
);
|
||||
if let Ok(program) = program {
|
||||
let header = program.header();
|
||||
assert!(header.version() <= 4, "not supported 5");
|
||||
let line_encoding = LineEncoding {
|
||||
minimum_instruction_length: header.minimum_instruction_length(),
|
||||
maximum_operations_per_instruction: header.maximum_operations_per_instruction(),
|
||||
default_is_stmt: header.default_is_stmt(),
|
||||
line_base: header.line_base(),
|
||||
line_range: header.line_range(),
|
||||
};
|
||||
let mut out_program = write::LineProgram::new(
|
||||
out_encoding,
|
||||
line_encoding,
|
||||
out_comp_dir,
|
||||
out_comp_name,
|
||||
None,
|
||||
);
|
||||
let mut dirs = Vec::new();
|
||||
dirs.push(out_program.default_directory());
|
||||
for dir_attr in header.include_directories() {
|
||||
let dir_id = out_program.add_directory(clone_attr_string(
|
||||
dir_attr,
|
||||
gimli::DW_FORM_string,
|
||||
debug_str,
|
||||
out_strings,
|
||||
)?);
|
||||
dirs.push(dir_id);
|
||||
}
|
||||
let mut files = Vec::new();
|
||||
for file_entry in header.file_names() {
|
||||
let dir_id = dirs[file_entry.directory_index() as usize];
|
||||
let file_id = out_program.add_file(
|
||||
clone_attr_string(
|
||||
&file_entry.path_name(),
|
||||
gimli::DW_FORM_string,
|
||||
debug_str,
|
||||
out_strings,
|
||||
)?,
|
||||
dir_id,
|
||||
None,
|
||||
);
|
||||
files.push(file_id);
|
||||
}
|
||||
|
||||
let mut rows = program.rows();
|
||||
let mut saved_rows = BTreeMap::new();
|
||||
let mut state = ReadLineProgramState::SequenceEnded;
|
||||
while let Some((_header, row)) = rows.next_row()? {
|
||||
if state == ReadLineProgramState::IgnoreSequence {
|
||||
if row.end_sequence() {
|
||||
state = ReadLineProgramState::SequenceEnded;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let saved_row = if row.end_sequence() {
|
||||
state = ReadLineProgramState::SequenceEnded;
|
||||
SavedLineProgramRow::EndOfSequence(row.address())
|
||||
} else {
|
||||
if state == ReadLineProgramState::SequenceEnded {
|
||||
// Discard sequences for non-existent code.
|
||||
if row.address() == 0 {
|
||||
state = ReadLineProgramState::IgnoreSequence;
|
||||
continue;
|
||||
}
|
||||
state = ReadLineProgramState::ReadSequence;
|
||||
}
|
||||
SavedLineProgramRow::Normal {
|
||||
address: row.address(),
|
||||
op_index: row.op_index(),
|
||||
file_index: row.file_index(),
|
||||
line: row.line().unwrap_or(0),
|
||||
column: match row.column() {
|
||||
gimli::ColumnType::LeftEdge => 0,
|
||||
gimli::ColumnType::Column(val) => val,
|
||||
},
|
||||
discriminator: row.discriminator(),
|
||||
is_stmt: row.is_stmt(),
|
||||
basic_block: row.basic_block(),
|
||||
prologue_end: row.prologue_end(),
|
||||
epilogue_begin: row.epilogue_begin(),
|
||||
isa: row.isa(),
|
||||
}
|
||||
};
|
||||
saved_rows.insert(row.address(), saved_row);
|
||||
}
|
||||
|
||||
let saved_rows = Vec::from_iter(saved_rows.into_iter());
|
||||
for (i, map) in addr_tr.map() {
|
||||
if map.len == 0 {
|
||||
continue; // no code generated
|
||||
}
|
||||
let symbol = i.index();
|
||||
let base_addr = map.offset;
|
||||
out_program.begin_sequence(Some(write::Address::Symbol { symbol, addend: 0 }));
|
||||
// TODO track and place function declaration line here
|
||||
let mut last_address = None;
|
||||
for addr_map in map.addresses.iter() {
|
||||
let saved_row = match saved_rows.binary_search_by_key(&addr_map.wasm, |i| i.0) {
|
||||
Ok(i) => Some(&saved_rows[i].1),
|
||||
Err(i) => {
|
||||
if i > 0 {
|
||||
Some(&saved_rows[i - 1].1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
if let Some(SavedLineProgramRow::Normal {
|
||||
address,
|
||||
op_index,
|
||||
file_index,
|
||||
line,
|
||||
column,
|
||||
discriminator,
|
||||
is_stmt,
|
||||
basic_block,
|
||||
prologue_end,
|
||||
epilogue_begin,
|
||||
isa,
|
||||
}) = saved_row
|
||||
{
|
||||
// Ignore duplicates
|
||||
if Some(*address) != last_address {
|
||||
let address_offset = if last_address.is_none() {
|
||||
// Extend first entry to the function declaration
|
||||
// TODO use the function declaration line instead
|
||||
0
|
||||
} else {
|
||||
(addr_map.generated - base_addr) as u64
|
||||
};
|
||||
out_program.row().address_offset = address_offset;
|
||||
out_program.row().op_index = *op_index;
|
||||
out_program.row().file = files[(file_index - 1) as usize];
|
||||
out_program.row().line = *line;
|
||||
out_program.row().column = *column;
|
||||
out_program.row().discriminator = *discriminator;
|
||||
out_program.row().is_statement = *is_stmt;
|
||||
out_program.row().basic_block = *basic_block;
|
||||
out_program.row().prologue_end = *prologue_end;
|
||||
out_program.row().epilogue_begin = *epilogue_begin;
|
||||
out_program.row().isa = *isa;
|
||||
out_program.generate_row();
|
||||
last_address = Some(*address);
|
||||
}
|
||||
}
|
||||
}
|
||||
let end_addr = (map.offset + map.len - 1) as u64;
|
||||
out_program.end_sequence(end_addr);
|
||||
}
|
||||
Ok((out_program, offset, files))
|
||||
} else {
|
||||
Err(TransformError("Valid line program not found").into())
|
||||
}
|
||||
}
|
||||
119
crates/debug/src/transform/mod.rs
Normal file
119
crates/debug/src/transform/mod.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use crate::gc::build_dependencies;
|
||||
use crate::DebugInfoData;
|
||||
use crate::HashSet;
|
||||
use cranelift_codegen::isa::TargetFrontendConfig;
|
||||
use failure::Error;
|
||||
use simulate::generate_simulated_dwarf;
|
||||
use thiserror::Error;
|
||||
use wasmtime_environ::{ModuleAddressMap, ModuleVmctxInfo, ValueLabelsRanges};
|
||||
|
||||
use gimli;
|
||||
|
||||
use gimli::{
|
||||
DebugAddr, DebugAddrBase, DebugLine, DebugStr, LocationLists, RangeLists, UnitSectionOffset,
|
||||
};
|
||||
|
||||
use gimli::write;
|
||||
|
||||
pub use address_transform::AddressTransform;
|
||||
|
||||
use unit::clone_unit;
|
||||
|
||||
mod address_transform;
|
||||
mod attr;
|
||||
mod expression;
|
||||
mod line_program;
|
||||
mod range_info_builder;
|
||||
mod simulate;
|
||||
mod unit;
|
||||
mod utils;
|
||||
|
||||
pub(crate) trait Reader: gimli::Reader<Offset = usize> {}
|
||||
|
||||
impl<'input, Endian> Reader for gimli::EndianSlice<'input, Endian> where Endian: gimli::Endianity {}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[error("Debug info transform error: {0}")]
|
||||
pub struct TransformError(&'static str);
|
||||
|
||||
pub(crate) struct DebugInputContext<'a, R>
|
||||
where
|
||||
R: Reader,
|
||||
{
|
||||
debug_str: &'a DebugStr<R>,
|
||||
debug_line: &'a DebugLine<R>,
|
||||
debug_addr: &'a DebugAddr<R>,
|
||||
debug_addr_base: DebugAddrBase<R::Offset>,
|
||||
rnglists: &'a RangeLists<R>,
|
||||
loclists: &'a LocationLists<R>,
|
||||
reachable: &'a HashSet<UnitSectionOffset>,
|
||||
}
|
||||
|
||||
pub fn transform_dwarf(
|
||||
target_config: &TargetFrontendConfig,
|
||||
di: &DebugInfoData,
|
||||
at: &ModuleAddressMap,
|
||||
vmctx_info: &ModuleVmctxInfo,
|
||||
ranges: &ValueLabelsRanges,
|
||||
) -> Result<write::Dwarf, Error> {
|
||||
let addr_tr = AddressTransform::new(at, &di.wasm_file);
|
||||
let reachable = build_dependencies(&di.dwarf, &addr_tr)?.get_reachable();
|
||||
|
||||
let context = DebugInputContext {
|
||||
debug_str: &di.dwarf.debug_str,
|
||||
debug_line: &di.dwarf.debug_line,
|
||||
debug_addr: &di.dwarf.debug_addr,
|
||||
debug_addr_base: DebugAddrBase(0),
|
||||
rnglists: &di.dwarf.ranges,
|
||||
loclists: &di.dwarf.locations,
|
||||
reachable: &reachable,
|
||||
};
|
||||
|
||||
let out_encoding = gimli::Encoding {
|
||||
format: gimli::Format::Dwarf32,
|
||||
// TODO: this should be configurable
|
||||
// macOS doesn't seem to support DWARF > 3
|
||||
version: 3,
|
||||
address_size: target_config.pointer_bytes(),
|
||||
};
|
||||
|
||||
let mut out_strings = write::StringTable::default();
|
||||
let mut out_units = write::UnitTable::default();
|
||||
|
||||
let out_line_strings = write::LineStringTable::default();
|
||||
|
||||
let mut translated = HashSet::new();
|
||||
let mut iter = di.dwarf.debug_info.units();
|
||||
while let Some(unit) = iter.next().unwrap_or(None) {
|
||||
let unit = di.dwarf.unit(unit)?;
|
||||
clone_unit(
|
||||
unit,
|
||||
&context,
|
||||
&addr_tr,
|
||||
&ranges,
|
||||
out_encoding,
|
||||
&vmctx_info,
|
||||
&mut out_units,
|
||||
&mut out_strings,
|
||||
&mut translated,
|
||||
)?;
|
||||
}
|
||||
|
||||
generate_simulated_dwarf(
|
||||
&addr_tr,
|
||||
di,
|
||||
&vmctx_info,
|
||||
&ranges,
|
||||
&translated,
|
||||
out_encoding,
|
||||
&mut out_units,
|
||||
&mut out_strings,
|
||||
)?;
|
||||
|
||||
Ok(write::Dwarf {
|
||||
units: out_units,
|
||||
line_programs: vec![],
|
||||
line_strings: out_line_strings,
|
||||
strings: out_strings,
|
||||
})
|
||||
}
|
||||
226
crates/debug/src/transform/range_info_builder.rs
Normal file
226
crates/debug/src/transform/range_info_builder.rs
Normal file
@@ -0,0 +1,226 @@
|
||||
use alloc::vec::Vec;
|
||||
use cranelift_entity::EntityRef;
|
||||
use cranelift_wasm::DefinedFuncIndex;
|
||||
use failure::Error;
|
||||
|
||||
use gimli;
|
||||
|
||||
use gimli::{AttributeValue, DebuggingInformationEntry, RangeListsOffset};
|
||||
|
||||
use gimli::write;
|
||||
|
||||
use super::address_transform::AddressTransform;
|
||||
use super::DebugInputContext;
|
||||
use super::Reader;
|
||||
|
||||
pub(crate) enum RangeInfoBuilder {
|
||||
Undefined,
|
||||
Position(u64),
|
||||
Ranges(Vec<(u64, u64)>),
|
||||
Function(DefinedFuncIndex),
|
||||
}
|
||||
|
||||
impl RangeInfoBuilder {
|
||||
pub(crate) fn from<R>(
|
||||
entry: &DebuggingInformationEntry<R>,
|
||||
context: &DebugInputContext<R>,
|
||||
unit_encoding: gimli::Encoding,
|
||||
cu_low_pc: u64,
|
||||
) -> Result<Self, Error>
|
||||
where
|
||||
R: Reader,
|
||||
{
|
||||
if let Some(AttributeValue::RangeListsRef(r)) = entry.attr_value(gimli::DW_AT_ranges)? {
|
||||
return RangeInfoBuilder::from_ranges_ref(r, context, unit_encoding, cu_low_pc);
|
||||
};
|
||||
|
||||
let low_pc =
|
||||
if let Some(AttributeValue::Addr(addr)) = entry.attr_value(gimli::DW_AT_low_pc)? {
|
||||
addr
|
||||
} else {
|
||||
return Ok(RangeInfoBuilder::Undefined);
|
||||
};
|
||||
|
||||
Ok(
|
||||
if let Some(AttributeValue::Udata(u)) = entry.attr_value(gimli::DW_AT_high_pc)? {
|
||||
RangeInfoBuilder::Ranges(vec![(low_pc, low_pc + u)])
|
||||
} else {
|
||||
RangeInfoBuilder::Position(low_pc)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn from_ranges_ref<R>(
|
||||
ranges: RangeListsOffset,
|
||||
context: &DebugInputContext<R>,
|
||||
unit_encoding: gimli::Encoding,
|
||||
cu_low_pc: u64,
|
||||
) -> Result<Self, Error>
|
||||
where
|
||||
R: Reader,
|
||||
{
|
||||
let mut ranges = context.rnglists.ranges(
|
||||
ranges,
|
||||
unit_encoding,
|
||||
cu_low_pc,
|
||||
&context.debug_addr,
|
||||
context.debug_addr_base,
|
||||
)?;
|
||||
let mut result = Vec::new();
|
||||
while let Some(range) = ranges.next()? {
|
||||
if range.begin >= range.end {
|
||||
// ignore empty ranges
|
||||
}
|
||||
result.push((range.begin, range.end));
|
||||
}
|
||||
|
||||
Ok(if result.len() > 0 {
|
||||
RangeInfoBuilder::Ranges(result)
|
||||
} else {
|
||||
RangeInfoBuilder::Undefined
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn from_subprogram_die<R>(
|
||||
entry: &DebuggingInformationEntry<R>,
|
||||
context: &DebugInputContext<R>,
|
||||
unit_encoding: gimli::Encoding,
|
||||
addr_tr: &AddressTransform,
|
||||
cu_low_pc: u64,
|
||||
) -> Result<Self, Error>
|
||||
where
|
||||
R: Reader,
|
||||
{
|
||||
let addr =
|
||||
if let Some(AttributeValue::Addr(addr)) = entry.attr_value(gimli::DW_AT_low_pc)? {
|
||||
addr
|
||||
} else if let Some(AttributeValue::RangeListsRef(r)) =
|
||||
entry.attr_value(gimli::DW_AT_ranges)?
|
||||
{
|
||||
let mut ranges = context.rnglists.ranges(
|
||||
r,
|
||||
unit_encoding,
|
||||
cu_low_pc,
|
||||
&context.debug_addr,
|
||||
context.debug_addr_base,
|
||||
)?;
|
||||
if let Some(range) = ranges.next()? {
|
||||
range.begin
|
||||
} else {
|
||||
return Ok(RangeInfoBuilder::Undefined);
|
||||
}
|
||||
} else {
|
||||
return Ok(RangeInfoBuilder::Undefined);
|
||||
};
|
||||
|
||||
let index = addr_tr.find_func_index(addr);
|
||||
if index.is_none() {
|
||||
return Ok(RangeInfoBuilder::Undefined);
|
||||
}
|
||||
Ok(RangeInfoBuilder::Function(index.unwrap()))
|
||||
}
|
||||
|
||||
pub(crate) fn build(
|
||||
&self,
|
||||
addr_tr: &AddressTransform,
|
||||
out_unit: &mut write::Unit,
|
||||
current_scope_id: write::UnitEntryId,
|
||||
) {
|
||||
match self {
|
||||
RangeInfoBuilder::Undefined => (),
|
||||
RangeInfoBuilder::Position(pc) => {
|
||||
let addr = addr_tr
|
||||
.translate(*pc)
|
||||
.unwrap_or(write::Address::Constant(0));
|
||||
let current_scope = out_unit.get_mut(current_scope_id);
|
||||
current_scope.set(gimli::DW_AT_low_pc, write::AttributeValue::Address(addr));
|
||||
}
|
||||
RangeInfoBuilder::Ranges(ranges) => {
|
||||
let mut result = Vec::new();
|
||||
for (begin, end) in ranges {
|
||||
for tr in addr_tr.translate_ranges(*begin, *end) {
|
||||
if tr.1 == 0 {
|
||||
// Ignore empty range
|
||||
continue;
|
||||
}
|
||||
result.push(tr);
|
||||
}
|
||||
}
|
||||
if result.len() != 1 {
|
||||
let range_list = result
|
||||
.iter()
|
||||
.map(|tr| write::Range::StartLength {
|
||||
begin: tr.0,
|
||||
length: tr.1,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let range_list_id = out_unit.ranges.add(write::RangeList(range_list));
|
||||
let current_scope = out_unit.get_mut(current_scope_id);
|
||||
current_scope.set(
|
||||
gimli::DW_AT_ranges,
|
||||
write::AttributeValue::RangeListRef(range_list_id),
|
||||
);
|
||||
} else {
|
||||
let current_scope = out_unit.get_mut(current_scope_id);
|
||||
current_scope.set(
|
||||
gimli::DW_AT_low_pc,
|
||||
write::AttributeValue::Address(result[0].0),
|
||||
);
|
||||
current_scope.set(
|
||||
gimli::DW_AT_high_pc,
|
||||
write::AttributeValue::Udata(result[0].1),
|
||||
);
|
||||
}
|
||||
}
|
||||
RangeInfoBuilder::Function(index) => {
|
||||
let range = addr_tr.func_range(*index);
|
||||
let symbol = index.index();
|
||||
let addr = write::Address::Symbol {
|
||||
symbol,
|
||||
addend: range.0 as i64,
|
||||
};
|
||||
let len = (range.1 - range.0) as u64;
|
||||
let current_scope = out_unit.get_mut(current_scope_id);
|
||||
current_scope.set(gimli::DW_AT_low_pc, write::AttributeValue::Address(addr));
|
||||
current_scope.set(gimli::DW_AT_high_pc, write::AttributeValue::Udata(len));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_ranges(&self, addr_tr: &AddressTransform) -> Vec<(u64, u64)> {
|
||||
match self {
|
||||
RangeInfoBuilder::Undefined | RangeInfoBuilder::Position(_) => vec![],
|
||||
RangeInfoBuilder::Ranges(ranges) => ranges.clone(),
|
||||
RangeInfoBuilder::Function(index) => {
|
||||
let range = addr_tr.func_source_range(*index);
|
||||
vec![(range.0, range.1)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_ranges(
|
||||
&self,
|
||||
addr_tr: &AddressTransform,
|
||||
out_range_lists: &mut write::RangeListTable,
|
||||
) -> write::RangeListId {
|
||||
if let RangeInfoBuilder::Ranges(ranges) = self {
|
||||
let mut range_list = Vec::new();
|
||||
for (begin, end) in ranges {
|
||||
assert!(begin < end);
|
||||
for tr in addr_tr.translate_ranges(*begin, *end) {
|
||||
if tr.1 == 0 {
|
||||
// Ignore empty range
|
||||
continue;
|
||||
}
|
||||
range_list.push(write::Range::StartLength {
|
||||
begin: tr.0,
|
||||
length: tr.1,
|
||||
});
|
||||
}
|
||||
}
|
||||
out_range_lists.add(write::RangeList(range_list))
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
}
|
||||
372
crates/debug/src/transform/simulate.rs
Normal file
372
crates/debug/src/transform/simulate.rs
Normal file
@@ -0,0 +1,372 @@
|
||||
use crate::read_debuginfo::WasmFileInfo;
|
||||
pub use crate::read_debuginfo::{DebugInfoData, FunctionMetadata, WasmType};
|
||||
use crate::{HashMap, HashSet};
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use cranelift_entity::EntityRef;
|
||||
use cranelift_wasm::get_vmctx_value_label;
|
||||
use failure::Error;
|
||||
use std::path::PathBuf;
|
||||
use wasmtime_environ::{ModuleVmctxInfo, ValueLabelsRanges};
|
||||
|
||||
use gimli::write;
|
||||
use gimli::{self, LineEncoding};
|
||||
|
||||
use super::expression::{CompiledExpression, FunctionFrameInfo};
|
||||
use super::utils::{add_internal_types, append_vmctx_info, get_function_frame_info};
|
||||
use super::AddressTransform;
|
||||
|
||||
const PRODUCER_NAME: &str = "wasmtime";
|
||||
|
||||
fn generate_line_info(
|
||||
addr_tr: &AddressTransform,
|
||||
translated: &HashSet<u32>,
|
||||
out_encoding: gimli::Encoding,
|
||||
w: &WasmFileInfo,
|
||||
comp_dir_id: write::StringId,
|
||||
name_id: write::StringId,
|
||||
name: &str,
|
||||
) -> Result<write::LineProgram, Error> {
|
||||
let out_comp_dir = write::LineString::StringRef(comp_dir_id);
|
||||
let out_comp_name = write::LineString::StringRef(name_id);
|
||||
|
||||
let line_encoding = LineEncoding::default();
|
||||
|
||||
let mut out_program = write::LineProgram::new(
|
||||
out_encoding,
|
||||
line_encoding,
|
||||
out_comp_dir,
|
||||
out_comp_name,
|
||||
None,
|
||||
);
|
||||
|
||||
let file_index = out_program.add_file(
|
||||
write::LineString::String(name.as_bytes().to_vec()),
|
||||
out_program.default_directory(),
|
||||
None,
|
||||
);
|
||||
|
||||
for (i, map) in addr_tr.map() {
|
||||
let symbol = i.index();
|
||||
if translated.contains(&(symbol as u32)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let base_addr = map.offset;
|
||||
out_program.begin_sequence(Some(write::Address::Symbol { symbol, addend: 0 }));
|
||||
for addr_map in map.addresses.iter() {
|
||||
let address_offset = (addr_map.generated - base_addr) as u64;
|
||||
out_program.row().address_offset = address_offset;
|
||||
out_program.row().op_index = 0;
|
||||
out_program.row().file = file_index;
|
||||
let wasm_offset = w.code_section_offset + addr_map.wasm as u64;
|
||||
out_program.row().line = wasm_offset;
|
||||
out_program.row().column = 0;
|
||||
out_program.row().discriminator = 1;
|
||||
out_program.row().is_statement = true;
|
||||
out_program.row().basic_block = false;
|
||||
out_program.row().prologue_end = false;
|
||||
out_program.row().epilogue_begin = false;
|
||||
out_program.row().isa = 0;
|
||||
out_program.generate_row();
|
||||
}
|
||||
let end_addr = (map.offset + map.len - 1) as u64;
|
||||
out_program.end_sequence(end_addr);
|
||||
}
|
||||
|
||||
Ok(out_program)
|
||||
}
|
||||
|
||||
fn autogenerate_dwarf_wasm_path(di: &DebugInfoData) -> PathBuf {
|
||||
let module_name = di
|
||||
.name_section
|
||||
.as_ref()
|
||||
.and_then(|ns| ns.module_name.to_owned())
|
||||
.unwrap_or_else(|| unsafe {
|
||||
static mut GEN_ID: u32 = 0;
|
||||
GEN_ID += 1;
|
||||
format!("<gen-{}>", GEN_ID)
|
||||
});
|
||||
let path = format!("/<wasm-module>/{}.wasm", module_name);
|
||||
PathBuf::from(path)
|
||||
}
|
||||
|
||||
struct WasmTypesDieRefs {
|
||||
vmctx: write::UnitEntryId,
|
||||
i32: write::UnitEntryId,
|
||||
i64: write::UnitEntryId,
|
||||
f32: write::UnitEntryId,
|
||||
f64: write::UnitEntryId,
|
||||
}
|
||||
|
||||
fn add_wasm_types(
|
||||
unit: &mut write::Unit,
|
||||
root_id: write::UnitEntryId,
|
||||
out_strings: &mut write::StringTable,
|
||||
vmctx_info: &ModuleVmctxInfo,
|
||||
) -> WasmTypesDieRefs {
|
||||
let (_wp_die_id, vmctx_die_id) = add_internal_types(unit, root_id, out_strings, vmctx_info);
|
||||
|
||||
macro_rules! def_type {
|
||||
($id:literal, $size:literal, $enc:path) => {{
|
||||
let die_id = unit.add(root_id, gimli::DW_TAG_base_type);
|
||||
let die = unit.get_mut(die_id);
|
||||
die.set(
|
||||
gimli::DW_AT_name,
|
||||
write::AttributeValue::StringRef(out_strings.add($id)),
|
||||
);
|
||||
die.set(gimli::DW_AT_byte_size, write::AttributeValue::Data1($size));
|
||||
die.set(gimli::DW_AT_encoding, write::AttributeValue::Encoding($enc));
|
||||
die_id
|
||||
}};
|
||||
}
|
||||
|
||||
let i32_die_id = def_type!("i32", 4, gimli::DW_ATE_signed);
|
||||
let i64_die_id = def_type!("i64", 8, gimli::DW_ATE_signed);
|
||||
let f32_die_id = def_type!("f32", 4, gimli::DW_ATE_float);
|
||||
let f64_die_id = def_type!("f64", 8, gimli::DW_ATE_float);
|
||||
|
||||
WasmTypesDieRefs {
|
||||
vmctx: vmctx_die_id,
|
||||
i32: i32_die_id,
|
||||
i64: i64_die_id,
|
||||
f32: f32_die_id,
|
||||
f64: f64_die_id,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_var_type(
|
||||
index: usize,
|
||||
wasm_types: &WasmTypesDieRefs,
|
||||
func_meta: &FunctionMetadata,
|
||||
) -> Option<(write::UnitEntryId, bool)> {
|
||||
let (ty, is_param) = if index < func_meta.params.len() {
|
||||
(func_meta.params[index], true)
|
||||
} else {
|
||||
let mut i = (index - func_meta.params.len()) as u32;
|
||||
let mut j = 0;
|
||||
while j < func_meta.locals.len() && i >= func_meta.locals[j].0 {
|
||||
i -= func_meta.locals[j].0;
|
||||
j += 1;
|
||||
}
|
||||
if j >= func_meta.locals.len() {
|
||||
// Ignore the var index out of bound.
|
||||
return None;
|
||||
}
|
||||
(func_meta.locals[j].1, false)
|
||||
};
|
||||
let type_die_id = match ty {
|
||||
WasmType::I32 => wasm_types.i32,
|
||||
WasmType::I64 => wasm_types.i64,
|
||||
WasmType::F32 => wasm_types.f32,
|
||||
WasmType::F64 => wasm_types.f64,
|
||||
_ => {
|
||||
// Ignore unsupported types.
|
||||
return None;
|
||||
}
|
||||
};
|
||||
Some((type_die_id, is_param))
|
||||
}
|
||||
|
||||
fn generate_vars(
|
||||
unit: &mut write::Unit,
|
||||
die_id: write::UnitEntryId,
|
||||
addr_tr: &AddressTransform,
|
||||
frame_info: &FunctionFrameInfo,
|
||||
scope_ranges: &[(u64, u64)],
|
||||
wasm_types: &WasmTypesDieRefs,
|
||||
func_meta: &FunctionMetadata,
|
||||
locals_names: Option<&HashMap<u32, String>>,
|
||||
out_strings: &mut write::StringTable,
|
||||
) {
|
||||
let vmctx_label = get_vmctx_value_label();
|
||||
|
||||
for label in frame_info.value_ranges.keys() {
|
||||
if label.index() == vmctx_label.index() {
|
||||
append_vmctx_info(
|
||||
unit,
|
||||
die_id,
|
||||
wasm_types.vmctx,
|
||||
addr_tr,
|
||||
Some(frame_info),
|
||||
scope_ranges,
|
||||
out_strings,
|
||||
)
|
||||
.expect("append_vmctx_info success");
|
||||
} else {
|
||||
let var_index = label.index();
|
||||
let (type_die_id, is_param) =
|
||||
if let Some(result) = resolve_var_type(var_index, wasm_types, func_meta) {
|
||||
result
|
||||
} else {
|
||||
// Skipping if type of local cannot be detected.
|
||||
continue;
|
||||
};
|
||||
|
||||
let loc_list_id = {
|
||||
let endian = gimli::RunTimeEndian::Little;
|
||||
|
||||
let expr = CompiledExpression::from_label(*label);
|
||||
let mut locs = Vec::new();
|
||||
for (begin, length, data) in
|
||||
expr.build_with_locals(scope_ranges, addr_tr, Some(frame_info), endian)
|
||||
{
|
||||
locs.push(write::Location::StartLength {
|
||||
begin,
|
||||
length,
|
||||
data,
|
||||
});
|
||||
}
|
||||
unit.locations.add(write::LocationList(locs))
|
||||
};
|
||||
|
||||
let var_id = unit.add(
|
||||
die_id,
|
||||
if is_param {
|
||||
gimli::DW_TAG_formal_parameter
|
||||
} else {
|
||||
gimli::DW_TAG_variable
|
||||
},
|
||||
);
|
||||
let var = unit.get_mut(var_id);
|
||||
|
||||
let name_id = match locals_names.and_then(|m| m.get(&(var_index as u32))) {
|
||||
Some(n) => out_strings.add(n.to_owned()),
|
||||
None => out_strings.add(format!("var{}", var_index)),
|
||||
};
|
||||
|
||||
var.set(gimli::DW_AT_name, write::AttributeValue::StringRef(name_id));
|
||||
var.set(
|
||||
gimli::DW_AT_type,
|
||||
write::AttributeValue::ThisUnitEntryRef(type_die_id),
|
||||
);
|
||||
var.set(
|
||||
gimli::DW_AT_location,
|
||||
write::AttributeValue::LocationListRef(loc_list_id),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_simulated_dwarf(
|
||||
addr_tr: &AddressTransform,
|
||||
di: &DebugInfoData,
|
||||
vmctx_info: &ModuleVmctxInfo,
|
||||
ranges: &ValueLabelsRanges,
|
||||
translated: &HashSet<u32>,
|
||||
out_encoding: gimli::Encoding,
|
||||
out_units: &mut write::UnitTable,
|
||||
out_strings: &mut write::StringTable,
|
||||
) -> Result<(), Error> {
|
||||
let path = di
|
||||
.wasm_file
|
||||
.path
|
||||
.to_owned()
|
||||
.unwrap_or_else(|| autogenerate_dwarf_wasm_path(di));
|
||||
|
||||
let (func_names, locals_names) = if let Some(ref name_section) = di.name_section {
|
||||
(
|
||||
Some(&name_section.func_names),
|
||||
Some(&name_section.locals_names),
|
||||
)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
let (unit, root_id, name_id) = {
|
||||
let comp_dir_id = out_strings.add(path.parent().expect("path dir").to_str().unwrap());
|
||||
let name = path.file_name().expect("path name").to_str().unwrap();
|
||||
let name_id = out_strings.add(name);
|
||||
|
||||
let out_program = generate_line_info(
|
||||
addr_tr,
|
||||
translated,
|
||||
out_encoding,
|
||||
&di.wasm_file,
|
||||
comp_dir_id,
|
||||
name_id,
|
||||
name,
|
||||
)?;
|
||||
|
||||
let unit_id = out_units.add(write::Unit::new(out_encoding, out_program));
|
||||
let unit = out_units.get_mut(unit_id);
|
||||
|
||||
let root_id = unit.root();
|
||||
let root = unit.get_mut(root_id);
|
||||
|
||||
let id = out_strings.add(PRODUCER_NAME);
|
||||
root.set(gimli::DW_AT_producer, write::AttributeValue::StringRef(id));
|
||||
root.set(gimli::DW_AT_name, write::AttributeValue::StringRef(name_id));
|
||||
root.set(
|
||||
gimli::DW_AT_stmt_list,
|
||||
write::AttributeValue::LineProgramRef,
|
||||
);
|
||||
root.set(
|
||||
gimli::DW_AT_comp_dir,
|
||||
write::AttributeValue::StringRef(comp_dir_id),
|
||||
);
|
||||
(unit, root_id, name_id)
|
||||
};
|
||||
|
||||
let wasm_types = add_wasm_types(unit, root_id, out_strings, vmctx_info);
|
||||
|
||||
for (i, map) in addr_tr.map().iter() {
|
||||
let index = i.index();
|
||||
if translated.contains(&(index as u32)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let start = map.offset as u64;
|
||||
let end = start + map.len as u64;
|
||||
let die_id = unit.add(root_id, gimli::DW_TAG_subprogram);
|
||||
let die = unit.get_mut(die_id);
|
||||
die.set(
|
||||
gimli::DW_AT_low_pc,
|
||||
write::AttributeValue::Address(write::Address::Symbol {
|
||||
symbol: index,
|
||||
addend: start as i64,
|
||||
}),
|
||||
);
|
||||
die.set(
|
||||
gimli::DW_AT_high_pc,
|
||||
write::AttributeValue::Udata((end - start) as u64),
|
||||
);
|
||||
|
||||
let id = match func_names.and_then(|m| m.get(&(index as u32))) {
|
||||
Some(n) => out_strings.add(n.to_owned()),
|
||||
None => out_strings.add(format!("wasm-function[{}]", index)),
|
||||
};
|
||||
|
||||
die.set(gimli::DW_AT_name, write::AttributeValue::StringRef(id));
|
||||
|
||||
die.set(
|
||||
gimli::DW_AT_decl_file,
|
||||
write::AttributeValue::StringRef(name_id),
|
||||
);
|
||||
|
||||
let f = addr_tr.map().get(i).unwrap();
|
||||
let f_start = f.addresses[0].wasm;
|
||||
let wasm_offset = di.wasm_file.code_section_offset + f_start as u64;
|
||||
die.set(
|
||||
gimli::DW_AT_decl_file,
|
||||
write::AttributeValue::Udata(wasm_offset),
|
||||
);
|
||||
|
||||
if let Some(frame_info) = get_function_frame_info(vmctx_info, i, ranges) {
|
||||
let source_range = addr_tr.func_source_range(i);
|
||||
generate_vars(
|
||||
unit,
|
||||
die_id,
|
||||
addr_tr,
|
||||
&frame_info,
|
||||
&[(source_range.0, source_range.1)],
|
||||
&wasm_types,
|
||||
&di.wasm_file.funcs[index],
|
||||
locals_names.and_then(|m| m.get(&(index as u32))),
|
||||
out_strings,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
375
crates/debug/src/transform/unit.rs
Normal file
375
crates/debug/src/transform/unit.rs
Normal file
@@ -0,0 +1,375 @@
|
||||
use crate::{HashMap, HashSet};
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use cranelift_entity::EntityRef;
|
||||
use failure::Error;
|
||||
use wasmtime_environ::{ModuleVmctxInfo, ValueLabelsRanges};
|
||||
|
||||
use gimli;
|
||||
|
||||
use gimli::{AttributeValue, DebuggingInformationEntry, Unit, UnitOffset};
|
||||
|
||||
use gimli::write;
|
||||
|
||||
use super::address_transform::AddressTransform;
|
||||
use super::attr::{clone_die_attributes, FileAttributeContext};
|
||||
use super::expression::compile_expression;
|
||||
use super::line_program::clone_line_program;
|
||||
use super::range_info_builder::RangeInfoBuilder;
|
||||
use super::utils::{add_internal_types, append_vmctx_info, get_function_frame_info};
|
||||
use super::{DebugInputContext, Reader, TransformError};
|
||||
|
||||
pub(crate) type PendingDieRef = (write::UnitEntryId, gimli::DwAt, UnitOffset);
|
||||
|
||||
struct InheritedAttr<T> {
|
||||
stack: Vec<(usize, T)>,
|
||||
}
|
||||
|
||||
impl<T> InheritedAttr<T> {
|
||||
fn new() -> Self {
|
||||
InheritedAttr { stack: Vec::new() }
|
||||
}
|
||||
|
||||
fn update(&mut self, depth: usize) {
|
||||
while !self.stack.is_empty() && self.stack.last().unwrap().0 >= depth {
|
||||
self.stack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&mut self, depth: usize, value: T) {
|
||||
self.stack.push((depth, value));
|
||||
}
|
||||
|
||||
fn top(&self) -> Option<&T> {
|
||||
self.stack.last().map(|entry| &entry.1)
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.stack.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_base_type_name<R>(
|
||||
type_entry: &DebuggingInformationEntry<R>,
|
||||
unit: &Unit<R, R::Offset>,
|
||||
context: &DebugInputContext<R>,
|
||||
) -> Result<String, Error>
|
||||
where
|
||||
R: Reader,
|
||||
{
|
||||
// FIXME remove recursion.
|
||||
match type_entry.attr_value(gimli::DW_AT_type)? {
|
||||
Some(AttributeValue::UnitRef(ref offset)) => {
|
||||
let mut entries = unit.entries_at_offset(*offset)?;
|
||||
entries.next_entry()?;
|
||||
if let Some(die) = entries.current() {
|
||||
if let Some(AttributeValue::DebugStrRef(str_offset)) =
|
||||
die.attr_value(gimli::DW_AT_name)?
|
||||
{
|
||||
return Ok(String::from(
|
||||
context.debug_str.get_str(str_offset)?.to_string()?,
|
||||
));
|
||||
}
|
||||
match die.tag() {
|
||||
gimli::DW_TAG_const_type => {
|
||||
return Ok(format!("const {}", get_base_type_name(die, unit, context)?));
|
||||
}
|
||||
gimli::DW_TAG_pointer_type => {
|
||||
return Ok(format!("{}*", get_base_type_name(die, unit, context)?));
|
||||
}
|
||||
gimli::DW_TAG_reference_type => {
|
||||
return Ok(format!("{}&", get_base_type_name(die, unit, context)?));
|
||||
}
|
||||
gimli::DW_TAG_array_type => {
|
||||
return Ok(format!("{}[]", get_base_type_name(die, unit, context)?));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
Ok(String::from("??"))
|
||||
}
|
||||
|
||||
fn replace_pointer_type<R>(
|
||||
parent_id: write::UnitEntryId,
|
||||
comp_unit: &mut write::Unit,
|
||||
wp_die_id: write::UnitEntryId,
|
||||
entry: &DebuggingInformationEntry<R>,
|
||||
unit: &Unit<R, R::Offset>,
|
||||
context: &DebugInputContext<R>,
|
||||
out_strings: &mut write::StringTable,
|
||||
pending_die_refs: &mut Vec<(write::UnitEntryId, gimli::DwAt, UnitOffset)>,
|
||||
) -> Result<write::UnitEntryId, Error>
|
||||
where
|
||||
R: Reader,
|
||||
{
|
||||
let die_id = comp_unit.add(parent_id, gimli::DW_TAG_structure_type);
|
||||
let die = comp_unit.get_mut(die_id);
|
||||
|
||||
let name = format!(
|
||||
"WebAssemblyPtrWrapper<{}>",
|
||||
get_base_type_name(entry, unit, context)?
|
||||
);
|
||||
die.set(
|
||||
gimli::DW_AT_name,
|
||||
write::AttributeValue::StringRef(out_strings.add(name.as_str())),
|
||||
);
|
||||
die.set(gimli::DW_AT_byte_size, write::AttributeValue::Data1(4));
|
||||
|
||||
let p_die_id = comp_unit.add(die_id, gimli::DW_TAG_template_type_parameter);
|
||||
let p_die = comp_unit.get_mut(p_die_id);
|
||||
p_die.set(
|
||||
gimli::DW_AT_name,
|
||||
write::AttributeValue::StringRef(out_strings.add("T")),
|
||||
);
|
||||
p_die.set(
|
||||
gimli::DW_AT_type,
|
||||
write::AttributeValue::ThisUnitEntryRef(wp_die_id),
|
||||
);
|
||||
match entry.attr_value(gimli::DW_AT_type)? {
|
||||
Some(AttributeValue::UnitRef(ref offset)) => {
|
||||
pending_die_refs.push((p_die_id, gimli::DW_AT_type, *offset))
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
let m_die_id = comp_unit.add(die_id, gimli::DW_TAG_member);
|
||||
let m_die = comp_unit.get_mut(m_die_id);
|
||||
m_die.set(
|
||||
gimli::DW_AT_name,
|
||||
write::AttributeValue::StringRef(out_strings.add("__ptr")),
|
||||
);
|
||||
m_die.set(
|
||||
gimli::DW_AT_type,
|
||||
write::AttributeValue::ThisUnitEntryRef(wp_die_id),
|
||||
);
|
||||
m_die.set(
|
||||
gimli::DW_AT_data_member_location,
|
||||
write::AttributeValue::Data1(0),
|
||||
);
|
||||
Ok(die_id)
|
||||
}
|
||||
|
||||
pub(crate) fn clone_unit<'a, R>(
|
||||
unit: Unit<R, R::Offset>,
|
||||
context: &DebugInputContext<R>,
|
||||
addr_tr: &'a AddressTransform,
|
||||
value_ranges: &'a ValueLabelsRanges,
|
||||
out_encoding: gimli::Encoding,
|
||||
module_info: &ModuleVmctxInfo,
|
||||
out_units: &mut write::UnitTable,
|
||||
out_strings: &mut write::StringTable,
|
||||
translated: &mut HashSet<u32>,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
R: Reader,
|
||||
{
|
||||
let mut die_ref_map = HashMap::new();
|
||||
let mut pending_die_refs = Vec::new();
|
||||
let mut stack = Vec::new();
|
||||
|
||||
// Iterate over all of this compilation unit's entries.
|
||||
let mut entries = unit.entries();
|
||||
let (mut comp_unit, file_map, cu_low_pc, wp_die_id, vmctx_die_id) =
|
||||
if let Some((depth_delta, entry)) = entries.next_dfs()? {
|
||||
assert!(depth_delta == 0);
|
||||
let (out_line_program, debug_line_offset, file_map) = clone_line_program(
|
||||
&unit,
|
||||
entry,
|
||||
addr_tr,
|
||||
out_encoding,
|
||||
context.debug_str,
|
||||
context.debug_line,
|
||||
out_strings,
|
||||
)?;
|
||||
|
||||
if entry.tag() == gimli::DW_TAG_compile_unit {
|
||||
let unit_id = out_units.add(write::Unit::new(out_encoding, out_line_program));
|
||||
let comp_unit = out_units.get_mut(unit_id);
|
||||
|
||||
let root_id = comp_unit.root();
|
||||
die_ref_map.insert(entry.offset(), root_id);
|
||||
|
||||
let cu_low_pc = if let Some(AttributeValue::Addr(addr)) =
|
||||
entry.attr_value(gimli::DW_AT_low_pc)?
|
||||
{
|
||||
addr
|
||||
} else {
|
||||
// FIXME? return Err(TransformError("No low_pc for unit header").into());
|
||||
0
|
||||
};
|
||||
|
||||
clone_die_attributes(
|
||||
entry,
|
||||
context,
|
||||
addr_tr,
|
||||
None,
|
||||
unit.encoding(),
|
||||
comp_unit,
|
||||
root_id,
|
||||
None,
|
||||
None,
|
||||
cu_low_pc,
|
||||
out_strings,
|
||||
&die_ref_map,
|
||||
&mut pending_die_refs,
|
||||
FileAttributeContext::Root(Some(debug_line_offset)),
|
||||
)?;
|
||||
|
||||
let (wp_die_id, vmctx_die_id) =
|
||||
add_internal_types(comp_unit, root_id, out_strings, module_info);
|
||||
|
||||
stack.push(root_id);
|
||||
(comp_unit, file_map, cu_low_pc, wp_die_id, vmctx_die_id)
|
||||
} else {
|
||||
return Err(TransformError("Unexpected unit header").into());
|
||||
}
|
||||
} else {
|
||||
return Ok(()); // empty
|
||||
};
|
||||
let mut skip_at_depth = None;
|
||||
let mut current_frame_base = InheritedAttr::new();
|
||||
let mut current_value_range = InheritedAttr::new();
|
||||
let mut current_scope_ranges = InheritedAttr::new();
|
||||
while let Some((depth_delta, entry)) = entries.next_dfs()? {
|
||||
let depth_delta = if let Some((depth, cached)) = skip_at_depth {
|
||||
let new_depth = depth + depth_delta;
|
||||
if new_depth > 0 {
|
||||
skip_at_depth = Some((new_depth, cached));
|
||||
continue;
|
||||
}
|
||||
skip_at_depth = None;
|
||||
new_depth + cached
|
||||
} else {
|
||||
depth_delta
|
||||
};
|
||||
|
||||
if !context
|
||||
.reachable
|
||||
.contains(&entry.offset().to_unit_section_offset(&unit))
|
||||
{
|
||||
// entry is not reachable: discarding all its info.
|
||||
skip_at_depth = Some((0, depth_delta));
|
||||
continue;
|
||||
}
|
||||
|
||||
let new_stack_len = stack.len().wrapping_add(depth_delta as usize);
|
||||
current_frame_base.update(new_stack_len);
|
||||
current_scope_ranges.update(new_stack_len);
|
||||
current_value_range.update(new_stack_len);
|
||||
let range_builder = if entry.tag() == gimli::DW_TAG_subprogram {
|
||||
let range_builder = RangeInfoBuilder::from_subprogram_die(
|
||||
entry,
|
||||
context,
|
||||
unit.encoding(),
|
||||
addr_tr,
|
||||
cu_low_pc,
|
||||
)?;
|
||||
if let RangeInfoBuilder::Function(func_index) = range_builder {
|
||||
if let Some(frame_info) =
|
||||
get_function_frame_info(module_info, func_index, value_ranges)
|
||||
{
|
||||
current_value_range.push(new_stack_len, frame_info);
|
||||
}
|
||||
translated.insert(func_index.index() as u32);
|
||||
current_scope_ranges.push(new_stack_len, range_builder.get_ranges(addr_tr));
|
||||
Some(range_builder)
|
||||
} else {
|
||||
// FIXME current_scope_ranges.push()
|
||||
None
|
||||
}
|
||||
} else {
|
||||
let high_pc = entry.attr_value(gimli::DW_AT_high_pc)?;
|
||||
let ranges = entry.attr_value(gimli::DW_AT_ranges)?;
|
||||
if high_pc.is_some() || ranges.is_some() {
|
||||
let range_builder =
|
||||
RangeInfoBuilder::from(entry, context, unit.encoding(), cu_low_pc)?;
|
||||
current_scope_ranges.push(new_stack_len, range_builder.get_ranges(addr_tr));
|
||||
Some(range_builder)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
if depth_delta <= 0 {
|
||||
for _ in depth_delta..1 {
|
||||
stack.pop();
|
||||
}
|
||||
} else {
|
||||
assert!(depth_delta == 1);
|
||||
}
|
||||
|
||||
if let Some(AttributeValue::Exprloc(expr)) = entry.attr_value(gimli::DW_AT_frame_base)? {
|
||||
if let Some(expr) = compile_expression(&expr, unit.encoding(), None)? {
|
||||
current_frame_base.push(new_stack_len, expr);
|
||||
}
|
||||
}
|
||||
|
||||
let parent = stack.last().unwrap();
|
||||
|
||||
if entry.tag() == gimli::DW_TAG_pointer_type {
|
||||
// Wrap pointer types.
|
||||
// TODO reference types?
|
||||
let die_id = replace_pointer_type(
|
||||
*parent,
|
||||
comp_unit,
|
||||
wp_die_id,
|
||||
entry,
|
||||
&unit,
|
||||
context,
|
||||
out_strings,
|
||||
&mut pending_die_refs,
|
||||
)?;
|
||||
stack.push(die_id);
|
||||
assert!(stack.len() == new_stack_len);
|
||||
die_ref_map.insert(entry.offset(), die_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
let die_id = comp_unit.add(*parent, entry.tag());
|
||||
|
||||
stack.push(die_id);
|
||||
assert!(stack.len() == new_stack_len);
|
||||
die_ref_map.insert(entry.offset(), die_id);
|
||||
|
||||
clone_die_attributes(
|
||||
entry,
|
||||
context,
|
||||
addr_tr,
|
||||
current_value_range.top(),
|
||||
unit.encoding(),
|
||||
&mut comp_unit,
|
||||
die_id,
|
||||
range_builder,
|
||||
current_scope_ranges.top(),
|
||||
cu_low_pc,
|
||||
out_strings,
|
||||
&die_ref_map,
|
||||
&mut pending_die_refs,
|
||||
FileAttributeContext::Children(&file_map, current_frame_base.top()),
|
||||
)?;
|
||||
|
||||
if entry.tag() == gimli::DW_TAG_subprogram && !current_scope_ranges.is_empty() {
|
||||
append_vmctx_info(
|
||||
comp_unit,
|
||||
die_id,
|
||||
vmctx_die_id,
|
||||
addr_tr,
|
||||
current_value_range.top(),
|
||||
current_scope_ranges.top().expect("range"),
|
||||
out_strings,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
for (die_id, attr_name, offset) in pending_die_refs {
|
||||
let die = comp_unit.get_mut(die_id);
|
||||
if let Some(unit_id) = die_ref_map.get(&offset) {
|
||||
die.set(attr_name, write::AttributeValue::ThisUnitEntryRef(*unit_id));
|
||||
} else {
|
||||
// TODO check why loosing DIEs
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
155
crates/debug/src/transform/utils.rs
Normal file
155
crates/debug/src/transform/utils.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
use alloc::vec::Vec;
|
||||
use cranelift_wasm::DefinedFuncIndex;
|
||||
use failure::Error;
|
||||
use wasmtime_environ::{ModuleVmctxInfo, ValueLabelsRanges};
|
||||
|
||||
use gimli;
|
||||
use gimli::write;
|
||||
|
||||
use super::address_transform::AddressTransform;
|
||||
use super::expression::{CompiledExpression, FunctionFrameInfo};
|
||||
|
||||
pub(crate) fn add_internal_types(
|
||||
comp_unit: &mut write::Unit,
|
||||
root_id: write::UnitEntryId,
|
||||
out_strings: &mut write::StringTable,
|
||||
module_info: &ModuleVmctxInfo,
|
||||
) -> (write::UnitEntryId, write::UnitEntryId) {
|
||||
let wp_die_id = comp_unit.add(root_id, gimli::DW_TAG_base_type);
|
||||
let wp_die = comp_unit.get_mut(wp_die_id);
|
||||
wp_die.set(
|
||||
gimli::DW_AT_name,
|
||||
write::AttributeValue::StringRef(out_strings.add("WebAssemblyPtr")),
|
||||
);
|
||||
wp_die.set(gimli::DW_AT_byte_size, write::AttributeValue::Data1(4));
|
||||
wp_die.set(
|
||||
gimli::DW_AT_encoding,
|
||||
write::AttributeValue::Encoding(gimli::DW_ATE_unsigned),
|
||||
);
|
||||
|
||||
let memory_byte_die_id = comp_unit.add(root_id, gimli::DW_TAG_base_type);
|
||||
let memory_byte_die = comp_unit.get_mut(memory_byte_die_id);
|
||||
memory_byte_die.set(
|
||||
gimli::DW_AT_name,
|
||||
write::AttributeValue::StringRef(out_strings.add("u8")),
|
||||
);
|
||||
memory_byte_die.set(
|
||||
gimli::DW_AT_encoding,
|
||||
write::AttributeValue::Encoding(gimli::DW_ATE_unsigned),
|
||||
);
|
||||
memory_byte_die.set(gimli::DW_AT_byte_size, write::AttributeValue::Data1(1));
|
||||
|
||||
let memory_bytes_die_id = comp_unit.add(root_id, gimli::DW_TAG_pointer_type);
|
||||
let memory_bytes_die = comp_unit.get_mut(memory_bytes_die_id);
|
||||
memory_bytes_die.set(
|
||||
gimli::DW_AT_name,
|
||||
write::AttributeValue::StringRef(out_strings.add("u8*")),
|
||||
);
|
||||
memory_bytes_die.set(
|
||||
gimli::DW_AT_type,
|
||||
write::AttributeValue::ThisUnitEntryRef(memory_byte_die_id),
|
||||
);
|
||||
|
||||
let memory_offset = module_info.memory_offset;
|
||||
let vmctx_die_id = comp_unit.add(root_id, gimli::DW_TAG_structure_type);
|
||||
let vmctx_die = comp_unit.get_mut(vmctx_die_id);
|
||||
vmctx_die.set(
|
||||
gimli::DW_AT_name,
|
||||
write::AttributeValue::StringRef(out_strings.add("WasmtimeVMContext")),
|
||||
);
|
||||
vmctx_die.set(
|
||||
gimli::DW_AT_byte_size,
|
||||
write::AttributeValue::Data4(memory_offset as u32 + 8),
|
||||
);
|
||||
|
||||
let m_die_id = comp_unit.add(vmctx_die_id, gimli::DW_TAG_member);
|
||||
let m_die = comp_unit.get_mut(m_die_id);
|
||||
m_die.set(
|
||||
gimli::DW_AT_name,
|
||||
write::AttributeValue::StringRef(out_strings.add("memory")),
|
||||
);
|
||||
m_die.set(
|
||||
gimli::DW_AT_type,
|
||||
write::AttributeValue::ThisUnitEntryRef(memory_bytes_die_id),
|
||||
);
|
||||
m_die.set(
|
||||
gimli::DW_AT_data_member_location,
|
||||
write::AttributeValue::Udata(memory_offset as u64),
|
||||
);
|
||||
|
||||
let vmctx_ptr_die_id = comp_unit.add(root_id, gimli::DW_TAG_pointer_type);
|
||||
let vmctx_ptr_die = comp_unit.get_mut(vmctx_ptr_die_id);
|
||||
vmctx_ptr_die.set(
|
||||
gimli::DW_AT_name,
|
||||
write::AttributeValue::StringRef(out_strings.add("WasmtimeVMContext*")),
|
||||
);
|
||||
vmctx_ptr_die.set(
|
||||
gimli::DW_AT_type,
|
||||
write::AttributeValue::ThisUnitEntryRef(vmctx_die_id),
|
||||
);
|
||||
|
||||
(wp_die_id, vmctx_ptr_die_id)
|
||||
}
|
||||
|
||||
pub(crate) fn append_vmctx_info(
|
||||
comp_unit: &mut write::Unit,
|
||||
parent_id: write::UnitEntryId,
|
||||
vmctx_die_id: write::UnitEntryId,
|
||||
addr_tr: &AddressTransform,
|
||||
frame_info: Option<&FunctionFrameInfo>,
|
||||
scope_ranges: &[(u64, u64)],
|
||||
out_strings: &mut write::StringTable,
|
||||
) -> Result<(), Error> {
|
||||
let loc = {
|
||||
let endian = gimli::RunTimeEndian::Little;
|
||||
|
||||
let expr = CompiledExpression::vmctx();
|
||||
let mut locs = Vec::new();
|
||||
for (begin, length, data) in
|
||||
expr.build_with_locals(scope_ranges, addr_tr, frame_info, endian)
|
||||
{
|
||||
locs.push(write::Location::StartLength {
|
||||
begin,
|
||||
length,
|
||||
data,
|
||||
});
|
||||
}
|
||||
let list_id = comp_unit.locations.add(write::LocationList(locs));
|
||||
write::AttributeValue::LocationListRef(list_id)
|
||||
};
|
||||
|
||||
let var_die_id = comp_unit.add(parent_id, gimli::DW_TAG_variable);
|
||||
let var_die = comp_unit.get_mut(var_die_id);
|
||||
var_die.set(
|
||||
gimli::DW_AT_name,
|
||||
write::AttributeValue::StringRef(out_strings.add("__vmctx")),
|
||||
);
|
||||
var_die.set(
|
||||
gimli::DW_AT_type,
|
||||
write::AttributeValue::ThisUnitEntryRef(vmctx_die_id),
|
||||
);
|
||||
var_die.set(gimli::DW_AT_location, loc);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn get_function_frame_info<'a, 'b, 'c>(
|
||||
module_info: &'b ModuleVmctxInfo,
|
||||
func_index: DefinedFuncIndex,
|
||||
value_ranges: &'c ValueLabelsRanges,
|
||||
) -> Option<FunctionFrameInfo<'a>>
|
||||
where
|
||||
'b: 'a,
|
||||
'c: 'a,
|
||||
{
|
||||
if let Some(value_ranges) = value_ranges.get(func_index) {
|
||||
let frame_info = FunctionFrameInfo {
|
||||
value_ranges,
|
||||
memory_offset: module_info.memory_offset,
|
||||
stack_slots: &module_info.stack_slots[func_index],
|
||||
};
|
||||
Some(frame_info)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
147
crates/debug/src/write_debuginfo.rs
Normal file
147
crates/debug/src/write_debuginfo.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use gimli::write::{Address, Dwarf, EndianVec, Result, Sections, Writer};
|
||||
use gimli::{RunTimeEndian, SectionId};
|
||||
|
||||
use core::result;
|
||||
use faerie::artifact::{Decl, SectionKind};
|
||||
use faerie::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct DebugReloc {
|
||||
offset: u32,
|
||||
size: u8,
|
||||
name: String,
|
||||
addend: i64,
|
||||
}
|
||||
|
||||
pub enum ResolvedSymbol {
|
||||
PhysicalAddress(u64),
|
||||
Reloc { name: String, addend: i64 },
|
||||
}
|
||||
|
||||
pub trait SymbolResolver {
|
||||
fn resolve_symbol(&self, symbol: usize, addend: i64) -> ResolvedSymbol;
|
||||
}
|
||||
|
||||
pub fn emit_dwarf(
|
||||
artifact: &mut Artifact,
|
||||
mut dwarf: Dwarf,
|
||||
symbol_resolver: &dyn SymbolResolver,
|
||||
) -> result::Result<(), failure::Error> {
|
||||
let endian = RunTimeEndian::Little;
|
||||
|
||||
let mut sections = Sections::new(WriterRelocate::new(endian, symbol_resolver));
|
||||
dwarf.write(&mut sections)?;
|
||||
sections.for_each_mut(|id, s| -> result::Result<(), failure::Error> {
|
||||
artifact.declare_with(
|
||||
id.name(),
|
||||
Decl::section(SectionKind::Debug),
|
||||
s.writer.take(),
|
||||
)
|
||||
})?;
|
||||
sections.for_each_mut(|id, s| -> result::Result<(), failure::Error> {
|
||||
for reloc in &s.relocs {
|
||||
artifact.link_with(
|
||||
faerie::Link {
|
||||
from: id.name(),
|
||||
to: &reloc.name,
|
||||
at: u64::from(reloc.offset),
|
||||
},
|
||||
faerie::Reloc::Debug {
|
||||
size: reloc.size,
|
||||
addend: reloc.addend as i32,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WriterRelocate<'a> {
|
||||
relocs: Vec<DebugReloc>,
|
||||
writer: EndianVec<RunTimeEndian>,
|
||||
symbol_resolver: &'a dyn SymbolResolver,
|
||||
}
|
||||
|
||||
impl<'a> WriterRelocate<'a> {
|
||||
pub fn new(endian: RunTimeEndian, symbol_resolver: &'a dyn SymbolResolver) -> Self {
|
||||
WriterRelocate {
|
||||
relocs: Vec::new(),
|
||||
writer: EndianVec::new(endian),
|
||||
symbol_resolver,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Writer for WriterRelocate<'a> {
|
||||
type Endian = RunTimeEndian;
|
||||
|
||||
fn endian(&self) -> Self::Endian {
|
||||
self.writer.endian()
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.writer.len()
|
||||
}
|
||||
|
||||
fn write(&mut self, bytes: &[u8]) -> Result<()> {
|
||||
self.writer.write(bytes)
|
||||
}
|
||||
|
||||
fn write_at(&mut self, offset: usize, bytes: &[u8]) -> Result<()> {
|
||||
self.writer.write_at(offset, bytes)
|
||||
}
|
||||
|
||||
fn write_address(&mut self, address: Address, size: u8) -> Result<()> {
|
||||
match address {
|
||||
Address::Constant(val) => self.write_udata(val, size),
|
||||
Address::Symbol { symbol, addend } => {
|
||||
match self.symbol_resolver.resolve_symbol(symbol, addend as i64) {
|
||||
ResolvedSymbol::PhysicalAddress(addr) => self.write_udata(addr, size),
|
||||
ResolvedSymbol::Reloc { name, addend } => {
|
||||
let offset = self.len() as u64;
|
||||
self.relocs.push(DebugReloc {
|
||||
offset: offset as u32,
|
||||
size,
|
||||
name,
|
||||
addend,
|
||||
});
|
||||
self.write_udata(addend as u64, size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_offset(&mut self, val: usize, section: SectionId, size: u8) -> Result<()> {
|
||||
let offset = self.len() as u32;
|
||||
let name = section.name().to_string();
|
||||
self.relocs.push(DebugReloc {
|
||||
offset,
|
||||
size,
|
||||
name,
|
||||
addend: val as i64,
|
||||
});
|
||||
self.write_udata(val as u64, size)
|
||||
}
|
||||
|
||||
fn write_offset_at(
|
||||
&mut self,
|
||||
offset: usize,
|
||||
val: usize,
|
||||
section: SectionId,
|
||||
size: u8,
|
||||
) -> Result<()> {
|
||||
let name = section.name().to_string();
|
||||
self.relocs.push(DebugReloc {
|
||||
offset: offset as u32,
|
||||
size,
|
||||
name,
|
||||
addend: val as i64,
|
||||
});
|
||||
self.write_udata_at(offset, val as u64, size)
|
||||
}
|
||||
}
|
||||
3
crates/environ/.gitignore
vendored
Normal file
3
crates/environ/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
target/
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
56
crates/environ/Cargo.toml
Normal file
56
crates/environ/Cargo.toml
Normal file
@@ -0,0 +1,56 @@
|
||||
[package]
|
||||
name = "wasmtime-environ"
|
||||
version = "0.2.0"
|
||||
authors = ["The Wasmtime Project Developers"]
|
||||
description = "Standalone environment support for WebAsssembly code in Cranelift"
|
||||
repository = "https://github.com/CraneStation/wasmtime"
|
||||
documentation = "https://docs.rs/wasmtime-environ/"
|
||||
categories = ["wasm"]
|
||||
keywords = ["webassembly", "wasm"]
|
||||
license = "Apache-2.0 WITH LLVM-exception"
|
||||
readme = "README.md"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
cranelift-codegen = { version = "0.49", features = ["enable-serde"] }
|
||||
cranelift-entity = { version = "0.49", features = ["enable-serde"] }
|
||||
cranelift-wasm = { version = "0.49", features = ["enable-serde"] }
|
||||
lightbeam = { path = "../lightbeam", optional = true }
|
||||
indexmap = "1.0.2"
|
||||
rayon = "1.1"
|
||||
thiserror = "1.0.4"
|
||||
directories = "2.0.1"
|
||||
sha2 = "0.8.0"
|
||||
base64 = "0.10.1"
|
||||
serde = { version = "1.0.94", features = ["derive"] }
|
||||
bincode = "1.1.4"
|
||||
lazy_static = "1.3.0"
|
||||
spin = "0.5.0"
|
||||
log = { version = "0.4.8", default-features = false }
|
||||
zstd = "0.4"
|
||||
toml = "0.5"
|
||||
file-per-thread-logger = "0.1.1"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
winapi = "0.3.7"
|
||||
|
||||
[target.'cfg(not(target_os = "windows"))'.dependencies]
|
||||
libc = "0.2.60"
|
||||
errno = "0.2.4"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
target-lexicon = { version = "0.9.0", default-features = false }
|
||||
pretty_env_logger = "0.3.0"
|
||||
rand = { version = "0.7.0", features = ["small_rng"] }
|
||||
cranelift-codegen = { version = "0.49", features = ["enable-serde", "all-arch"] }
|
||||
filetime = "0.2.7"
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = ["cranelift-codegen/std", "cranelift-wasm/std"]
|
||||
core = ["cranelift-codegen/core", "cranelift-wasm/core"]
|
||||
|
||||
[badges]
|
||||
maintenance = { status = "experimental" }
|
||||
travis-ci = { repository = "CraneStation/wasmtime" }
|
||||
220
crates/environ/LICENSE
Normal file
220
crates/environ/LICENSE
Normal file
@@ -0,0 +1,220 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
--- LLVM Exceptions to the Apache 2.0 License ----
|
||||
|
||||
As an exception, if, as a result of your compiling your source code, portions
|
||||
of this Software are embedded into an Object form of such source code, you
|
||||
may redistribute such embedded portions in such Object form without complying
|
||||
with the conditions of Sections 4(a), 4(b) and 4(d) of the License.
|
||||
|
||||
In addition, if you combine or link compiled forms of this Software with
|
||||
software that is licensed under the GPLv2 ("Combined Software") and if a
|
||||
court of competent jurisdiction determines that the patent provision (Section
|
||||
3), the indemnity provision (Section 9) or other Section of the License
|
||||
conflicts with the conditions of the GPLv2, you may retroactively and
|
||||
prospectively choose to deem waived or otherwise exclude such Section(s) of
|
||||
the License, but only in their entirety and only with respect to the Combined
|
||||
Software.
|
||||
|
||||
6
crates/environ/README.md
Normal file
6
crates/environ/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
This is the `wasmtime-environ` crate, which contains the implementations
|
||||
of the `ModuleEnvironment` and `FuncEnvironment` traits from
|
||||
[`cranelift-wasm`](https://crates.io/crates/cranelift-wasm). They effectively
|
||||
implement an ABI for basic wasm compilation that defines how linear memories
|
||||
are allocated, how indirect calls work, and other details. They can be used
|
||||
for JITing, native object files, or other purposes.
|
||||
9
crates/environ/build.rs
Normal file
9
crates/environ/build.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
let git_rev = match Command::new("git").args(&["rev-parse", "HEAD"]).output() {
|
||||
Ok(output) => String::from_utf8(output.stdout).unwrap(),
|
||||
Err(_) => String::from("git-not-found"),
|
||||
};
|
||||
println!("cargo:rustc-env=GIT_REV={}", git_rev);
|
||||
}
|
||||
59
crates/environ/src/address_map.rs
Normal file
59
crates/environ/src/address_map.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
//! Data structures to provide transformation of the source
|
||||
// addresses of a WebAssembly module into the native code.
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use cranelift_codegen::ir;
|
||||
use cranelift_entity::PrimaryMap;
|
||||
use cranelift_wasm::DefinedFuncIndex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Single source location to generated address mapping.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct InstructionAddressMap {
|
||||
/// Original source location.
|
||||
pub srcloc: ir::SourceLoc,
|
||||
|
||||
/// Generated instructions offset.
|
||||
pub code_offset: usize,
|
||||
|
||||
/// Generated instructions length.
|
||||
pub code_len: usize,
|
||||
}
|
||||
|
||||
/// Function and its instructions addresses mappings.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct FunctionAddressMap {
|
||||
/// Instructions maps.
|
||||
/// The array is sorted by the InstructionAddressMap::code_offset field.
|
||||
pub instructions: Vec<InstructionAddressMap>,
|
||||
|
||||
/// Function start source location (normally declaration).
|
||||
pub start_srcloc: ir::SourceLoc,
|
||||
|
||||
/// Function end source location.
|
||||
pub end_srcloc: ir::SourceLoc,
|
||||
|
||||
/// Generated function body offset if applicable, otherwise 0.
|
||||
pub body_offset: usize,
|
||||
|
||||
/// Generated function body length.
|
||||
pub body_len: usize,
|
||||
}
|
||||
|
||||
/// Module functions addresses mappings.
|
||||
pub type ModuleAddressMap = PrimaryMap<DefinedFuncIndex, FunctionAddressMap>;
|
||||
|
||||
/// Value ranges for functions.
|
||||
pub type ValueLabelsRanges = PrimaryMap<DefinedFuncIndex, cranelift_codegen::ValueLabelsRanges>;
|
||||
|
||||
/// Stack slots for functions.
|
||||
pub type StackSlots = PrimaryMap<DefinedFuncIndex, ir::StackSlots>;
|
||||
|
||||
/// Module `vmctx` related info.
|
||||
pub struct ModuleVmctxInfo {
|
||||
/// The memory definition offset in the VMContext structure.
|
||||
pub memory_offset: i64,
|
||||
|
||||
/// The functions stack slots.
|
||||
pub stack_slots: StackSlots,
|
||||
}
|
||||
295
crates/environ/src/cache.rs
Normal file
295
crates/environ/src/cache.rs
Normal file
@@ -0,0 +1,295 @@
|
||||
use crate::address_map::{ModuleAddressMap, ValueLabelsRanges};
|
||||
use crate::compilation::{Compilation, Relocations, Traps};
|
||||
use crate::module::Module;
|
||||
use crate::module_environ::FunctionBodyData;
|
||||
use alloc::string::{String, ToString};
|
||||
use core::hash::Hasher;
|
||||
use cranelift_codegen::{ir, isa};
|
||||
use cranelift_entity::PrimaryMap;
|
||||
use cranelift_wasm::DefinedFuncIndex;
|
||||
use lazy_static::lazy_static;
|
||||
use log::{debug, trace, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[macro_use] // for tests
|
||||
mod config;
|
||||
mod worker;
|
||||
|
||||
use config::{cache_config, CacheConfig};
|
||||
pub use config::{create_new_config, init};
|
||||
use worker::{worker, Worker};
|
||||
|
||||
lazy_static! {
|
||||
static ref SELF_MTIME: String = {
|
||||
std::env::current_exe()
|
||||
.map_err(|_| warn!("Failed to get path of current executable"))
|
||||
.ok()
|
||||
.and_then(|path| {
|
||||
fs::metadata(&path)
|
||||
.map_err(|_| warn!("Failed to get metadata of current executable"))
|
||||
.ok()
|
||||
})
|
||||
.and_then(|metadata| {
|
||||
metadata
|
||||
.modified()
|
||||
.map_err(|_| warn!("Failed to get metadata of current executable"))
|
||||
.ok()
|
||||
})
|
||||
.map(|mtime| match mtime.duration_since(std::time::UNIX_EPOCH) {
|
||||
Ok(duration) => format!("{}", duration.as_millis()),
|
||||
Err(err) => format!("m{}", err.duration().as_millis()),
|
||||
})
|
||||
.unwrap_or_else(|| "no-mtime".to_string())
|
||||
};
|
||||
}
|
||||
|
||||
pub struct ModuleCacheEntry<'config, 'worker>(Option<ModuleCacheEntryInner<'config, 'worker>>);
|
||||
|
||||
struct ModuleCacheEntryInner<'config, 'worker> {
|
||||
mod_cache_path: PathBuf,
|
||||
cache_config: &'config CacheConfig,
|
||||
worker: &'worker Worker,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
pub struct ModuleCacheData {
|
||||
compilation: Compilation,
|
||||
relocations: Relocations,
|
||||
address_transforms: ModuleAddressMap,
|
||||
value_ranges: ValueLabelsRanges,
|
||||
stack_slots: PrimaryMap<DefinedFuncIndex, ir::StackSlots>,
|
||||
traps: Traps,
|
||||
}
|
||||
|
||||
type ModuleCacheDataTupleType = (
|
||||
Compilation,
|
||||
Relocations,
|
||||
ModuleAddressMap,
|
||||
ValueLabelsRanges,
|
||||
PrimaryMap<DefinedFuncIndex, ir::StackSlots>,
|
||||
Traps,
|
||||
);
|
||||
|
||||
struct Sha256Hasher(Sha256);
|
||||
|
||||
impl<'config, 'worker> ModuleCacheEntry<'config, 'worker> {
|
||||
pub fn new<'data>(
|
||||
module: &Module,
|
||||
function_body_inputs: &PrimaryMap<DefinedFuncIndex, FunctionBodyData<'data>>,
|
||||
isa: &dyn isa::TargetIsa,
|
||||
compiler_name: &str,
|
||||
generate_debug_info: bool,
|
||||
) -> Self {
|
||||
let cache_config = cache_config();
|
||||
if cache_config.enabled() {
|
||||
Self(Some(ModuleCacheEntryInner::new(
|
||||
module,
|
||||
function_body_inputs,
|
||||
isa,
|
||||
compiler_name,
|
||||
generate_debug_info,
|
||||
cache_config,
|
||||
worker(),
|
||||
)))
|
||||
} else {
|
||||
Self(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn from_inner<'data>(inner: ModuleCacheEntryInner<'config, 'worker>) -> Self {
|
||||
Self(Some(inner))
|
||||
}
|
||||
|
||||
pub fn get_data(&self) -> Option<ModuleCacheData> {
|
||||
if let Some(inner) = &self.0 {
|
||||
inner.get_data().map(|val| {
|
||||
inner.worker.on_cache_get_async(&inner.mod_cache_path); // call on success
|
||||
val
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_data(&self, data: &ModuleCacheData) {
|
||||
if let Some(inner) = &self.0 {
|
||||
inner.update_data(data).map(|val| {
|
||||
inner.worker.on_cache_update_async(&inner.mod_cache_path); // call on success
|
||||
val
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'config, 'worker> ModuleCacheEntryInner<'config, 'worker> {
|
||||
fn new<'data>(
|
||||
module: &Module,
|
||||
function_body_inputs: &PrimaryMap<DefinedFuncIndex, FunctionBodyData<'data>>,
|
||||
isa: &dyn isa::TargetIsa,
|
||||
compiler_name: &str,
|
||||
generate_debug_info: bool,
|
||||
cache_config: &'config CacheConfig,
|
||||
worker: &'worker Worker,
|
||||
) -> Self {
|
||||
let hash = Sha256Hasher::digest(module, function_body_inputs);
|
||||
let compiler_dir = if cfg!(debug_assertions) {
|
||||
format!(
|
||||
"{comp_name}-{comp_ver}-{comp_mtime}",
|
||||
comp_name = compiler_name,
|
||||
comp_ver = env!("GIT_REV"),
|
||||
comp_mtime = *SELF_MTIME,
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{comp_name}-{comp_ver}",
|
||||
comp_name = compiler_name,
|
||||
comp_ver = env!("GIT_REV"),
|
||||
)
|
||||
};
|
||||
let mod_filename = format!(
|
||||
"mod-{mod_hash}{mod_dbg}",
|
||||
mod_hash = base64::encode_config(&hash, base64::URL_SAFE_NO_PAD), // standard encoding uses '/' which can't be used for filename
|
||||
mod_dbg = if generate_debug_info { ".d" } else { "" },
|
||||
);
|
||||
let mod_cache_path = cache_config
|
||||
.directory()
|
||||
.join(isa.triple().to_string())
|
||||
.join(compiler_dir)
|
||||
.join(mod_filename);
|
||||
|
||||
Self {
|
||||
mod_cache_path,
|
||||
cache_config,
|
||||
worker,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_data(&self) -> Option<ModuleCacheData> {
|
||||
trace!("get_data() for path: {}", self.mod_cache_path.display());
|
||||
let compressed_cache_bytes = fs::read(&self.mod_cache_path).ok()?;
|
||||
let cache_bytes = zstd::decode_all(&compressed_cache_bytes[..])
|
||||
.map_err(|err| warn!("Failed to decompress cached code: {}", err))
|
||||
.ok()?;
|
||||
bincode::deserialize(&cache_bytes[..])
|
||||
.map_err(|err| warn!("Failed to deserialize cached code: {}", err))
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn update_data(&self, data: &ModuleCacheData) -> Option<()> {
|
||||
trace!("update_data() for path: {}", self.mod_cache_path.display());
|
||||
let serialized_data = bincode::serialize(&data)
|
||||
.map_err(|err| warn!("Failed to serialize cached code: {}", err))
|
||||
.ok()?;
|
||||
let compressed_data = zstd::encode_all(
|
||||
&serialized_data[..],
|
||||
self.cache_config.baseline_compression_level(),
|
||||
)
|
||||
.map_err(|err| warn!("Failed to compress cached code: {}", err))
|
||||
.ok()?;
|
||||
|
||||
// Optimize syscalls: first, try writing to disk. It should succeed in most cases.
|
||||
// Otherwise, try creating the cache directory and retry writing to the file.
|
||||
if fs_write_atomic(&self.mod_cache_path, "mod", &compressed_data) {
|
||||
return Some(());
|
||||
}
|
||||
|
||||
debug!(
|
||||
"Attempting to create the cache directory, because \
|
||||
failed to write cached code to disk, path: {}",
|
||||
self.mod_cache_path.display(),
|
||||
);
|
||||
|
||||
let cache_dir = self.mod_cache_path.parent().unwrap();
|
||||
fs::create_dir_all(cache_dir)
|
||||
.map_err(|err| {
|
||||
warn!(
|
||||
"Failed to create cache directory, path: {}, message: {}",
|
||||
cache_dir.display(),
|
||||
err
|
||||
)
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
if fs_write_atomic(&self.mod_cache_path, "mod", &compressed_data) {
|
||||
Some(())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ModuleCacheData {
|
||||
pub fn from_tuple(data: ModuleCacheDataTupleType) -> Self {
|
||||
Self {
|
||||
compilation: data.0,
|
||||
relocations: data.1,
|
||||
address_transforms: data.2,
|
||||
value_ranges: data.3,
|
||||
stack_slots: data.4,
|
||||
traps: data.5,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_tuple(self) -> ModuleCacheDataTupleType {
|
||||
(
|
||||
self.compilation,
|
||||
self.relocations,
|
||||
self.address_transforms,
|
||||
self.value_ranges,
|
||||
self.stack_slots,
|
||||
self.traps,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sha256Hasher {
|
||||
pub fn digest<'data>(
|
||||
module: &Module,
|
||||
function_body_inputs: &PrimaryMap<DefinedFuncIndex, FunctionBodyData<'data>>,
|
||||
) -> [u8; 32] {
|
||||
let mut hasher = Self(Sha256::new());
|
||||
module.hash_for_cache(function_body_inputs, &mut hasher);
|
||||
hasher.0.result().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Hasher for Sha256Hasher {
|
||||
fn finish(&self) -> u64 {
|
||||
panic!("Sha256Hasher doesn't support finish!");
|
||||
}
|
||||
|
||||
fn write(&mut self, bytes: &[u8]) {
|
||||
self.0.input(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
// Assumption: path inside cache directory.
|
||||
// Then, we don't have to use sound OS-specific exclusive file access.
|
||||
// Note: there's no need to remove temporary file here - cleanup task will do it later.
|
||||
fn fs_write_atomic(path: &Path, reason: &str, contents: &[u8]) -> bool {
|
||||
let lock_path = path.with_extension(format!("wip-atomic-write-{}", reason));
|
||||
fs::OpenOptions::new()
|
||||
.create_new(true) // atomic file creation (assumption: no one will open it without this flag)
|
||||
.write(true)
|
||||
.open(&lock_path)
|
||||
.and_then(|mut file| file.write_all(contents))
|
||||
// file should go out of scope and be closed at this point
|
||||
.and_then(|()| fs::rename(&lock_path, &path)) // atomic file rename
|
||||
.map_err(|err| {
|
||||
warn!(
|
||||
"Failed to write file with rename, lock path: {}, target path: {}, err: {}",
|
||||
lock_path.display(),
|
||||
path.display(),
|
||||
err
|
||||
)
|
||||
})
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
625
crates/environ/src/cache/config.rs
vendored
Normal file
625
crates/environ/src/cache/config.rs
vendored
Normal file
@@ -0,0 +1,625 @@
|
||||
//! Module for configuring the cache system.
|
||||
|
||||
use super::worker;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
use core::time::Duration;
|
||||
use directories::ProjectDirs;
|
||||
use lazy_static::lazy_static;
|
||||
use log::{debug, error, trace, warn};
|
||||
use serde::{
|
||||
de::{self, Deserializer},
|
||||
Deserialize,
|
||||
};
|
||||
use spin::Once;
|
||||
use std::fmt::Debug;
|
||||
use std::fs;
|
||||
use std::mem;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
// wrapped, so we have named section in config,
|
||||
// also, for possible future compatibility
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Config {
|
||||
cache: CacheConfig,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct CacheConfig {
|
||||
#[serde(skip)]
|
||||
errors: Vec<String>,
|
||||
|
||||
enabled: bool,
|
||||
directory: Option<PathBuf>,
|
||||
#[serde(
|
||||
default,
|
||||
rename = "worker-event-queue-size",
|
||||
deserialize_with = "deserialize_si_prefix"
|
||||
)]
|
||||
worker_event_queue_size: Option<u64>,
|
||||
#[serde(rename = "baseline-compression-level")]
|
||||
baseline_compression_level: Option<i32>,
|
||||
#[serde(rename = "optimized-compression-level")]
|
||||
optimized_compression_level: Option<i32>,
|
||||
#[serde(
|
||||
default,
|
||||
rename = "optimized-compression-usage-counter-threshold",
|
||||
deserialize_with = "deserialize_si_prefix"
|
||||
)]
|
||||
optimized_compression_usage_counter_threshold: Option<u64>,
|
||||
#[serde(
|
||||
default,
|
||||
rename = "cleanup-interval",
|
||||
deserialize_with = "deserialize_duration"
|
||||
)]
|
||||
cleanup_interval: Option<Duration>,
|
||||
#[serde(
|
||||
default,
|
||||
rename = "optimizing-compression-task-timeout",
|
||||
deserialize_with = "deserialize_duration"
|
||||
)]
|
||||
optimizing_compression_task_timeout: Option<Duration>,
|
||||
#[serde(
|
||||
default,
|
||||
rename = "allowed-clock-drift-for-files-from-future",
|
||||
deserialize_with = "deserialize_duration"
|
||||
)]
|
||||
allowed_clock_drift_for_files_from_future: Option<Duration>,
|
||||
#[serde(
|
||||
default,
|
||||
rename = "file-count-soft-limit",
|
||||
deserialize_with = "deserialize_si_prefix"
|
||||
)]
|
||||
file_count_soft_limit: Option<u64>,
|
||||
#[serde(
|
||||
default,
|
||||
rename = "files-total-size-soft-limit",
|
||||
deserialize_with = "deserialize_disk_space"
|
||||
)]
|
||||
files_total_size_soft_limit: Option<u64>,
|
||||
#[serde(
|
||||
default,
|
||||
rename = "file-count-limit-percent-if-deleting",
|
||||
deserialize_with = "deserialize_percent"
|
||||
)]
|
||||
file_count_limit_percent_if_deleting: Option<u8>,
|
||||
#[serde(
|
||||
default,
|
||||
rename = "files-total-size-limit-percent-if-deleting",
|
||||
deserialize_with = "deserialize_percent"
|
||||
)]
|
||||
files_total_size_limit_percent_if_deleting: Option<u8>,
|
||||
}
|
||||
|
||||
// Private static, so only internal function can access it.
|
||||
static CONFIG: Once<CacheConfig> = Once::new();
|
||||
static INIT_CALLED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Returns cache configuration.
|
||||
///
|
||||
/// If system has not been initialized, it disables it.
|
||||
/// You mustn't call init() after it.
|
||||
pub fn cache_config() -> &'static CacheConfig {
|
||||
CONFIG.call_once(CacheConfig::new_cache_disabled)
|
||||
}
|
||||
|
||||
/// Initializes the cache system. Should be called exactly once,
|
||||
/// and before using the cache system. Otherwise it can panic.
|
||||
/// Returns list of errors. If empty, initialization succeeded.
|
||||
pub fn init<P: AsRef<Path> + Debug>(
|
||||
enabled: bool,
|
||||
config_file: Option<P>,
|
||||
init_file_per_thread_logger: Option<&'static str>,
|
||||
) -> &'static Vec<String> {
|
||||
INIT_CALLED
|
||||
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
|
||||
.expect("Cache system init must be called at most once");
|
||||
assert!(
|
||||
CONFIG.r#try().is_none(),
|
||||
"Cache system init must be called before using the system."
|
||||
);
|
||||
let conf_file_str = format!("{:?}", config_file);
|
||||
let conf = CONFIG.call_once(|| CacheConfig::from_file(enabled, config_file));
|
||||
if conf.errors.is_empty() {
|
||||
if conf.enabled() {
|
||||
worker::init(init_file_per_thread_logger);
|
||||
}
|
||||
debug!("Cache init(\"{}\"): {:#?}", conf_file_str, conf)
|
||||
} else {
|
||||
error!(
|
||||
"Cache init(\"{}\"): errors: {:#?}",
|
||||
conf_file_str, conf.errors,
|
||||
)
|
||||
}
|
||||
&conf.errors
|
||||
}
|
||||
|
||||
/// Creates a new configuration file at specified path, or default path if None is passed.
|
||||
/// Fails if file already exists.
|
||||
pub fn create_new_config<P: AsRef<Path> + Debug>(
|
||||
config_file: Option<P>,
|
||||
) -> Result<PathBuf, String> {
|
||||
trace!("Creating new config file, path: {:?}", config_file);
|
||||
|
||||
let config_file = config_file.as_ref().map_or_else(
|
||||
|| DEFAULT_CONFIG_PATH.as_ref().map(|p| p.as_ref()),
|
||||
|p| Ok(p.as_ref()),
|
||||
)?;
|
||||
|
||||
if config_file.exists() {
|
||||
Err(format!(
|
||||
"Specified config file already exists! Path: {}",
|
||||
config_file.display()
|
||||
))?;
|
||||
}
|
||||
|
||||
let parent_dir = config_file
|
||||
.parent()
|
||||
.ok_or_else(|| format!("Invalid cache config path: {}", config_file.display()))?;
|
||||
|
||||
fs::create_dir_all(parent_dir).map_err(|err| {
|
||||
format!(
|
||||
"Failed to create config directory, config path: {}, error: {}",
|
||||
config_file.display(),
|
||||
err
|
||||
)
|
||||
})?;
|
||||
|
||||
let content = "\
|
||||
# Comment out certain settings to use default values.
|
||||
# For more settings, please refer to the documentation:
|
||||
# https://github.com/CraneStation/wasmtime/blob/master/CACHE_CONFIGURATION.md
|
||||
|
||||
[cache]
|
||||
enabled = true
|
||||
";
|
||||
|
||||
fs::write(&config_file, &content).map_err(|err| {
|
||||
format!(
|
||||
"Failed to flush config to the disk, path: {}, msg: {}",
|
||||
config_file.display(),
|
||||
err
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(config_file.to_path_buf())
|
||||
}
|
||||
|
||||
// permitted levels from: https://docs.rs/zstd/0.4.28+zstd.1.4.3/zstd/stream/write/struct.Encoder.html
|
||||
const ZSTD_COMPRESSION_LEVELS: std::ops::RangeInclusive<i32> = 0..=21;
|
||||
lazy_static! {
|
||||
static ref PROJECT_DIRS: Option<ProjectDirs> =
|
||||
ProjectDirs::from("", "CraneStation", "wasmtime");
|
||||
static ref DEFAULT_CONFIG_PATH: Result<PathBuf, String> = PROJECT_DIRS
|
||||
.as_ref()
|
||||
.map(|proj_dirs| proj_dirs.config_dir().join("wasmtime-cache-config.toml"))
|
||||
.ok_or_else(|| "Config file not specified and failed to get the default".to_string());
|
||||
}
|
||||
|
||||
// Default settings, you're welcome to tune them!
|
||||
// TODO: what do we want to warn users about?
|
||||
|
||||
// At the moment of writing, the modules couldn't depend on anothers,
|
||||
// so we have at most one module per wasmtime instance
|
||||
// if changed, update CACHE_CONFIGURATION.md
|
||||
const DEFAULT_WORKER_EVENT_QUEUE_SIZE: u64 = 0x10;
|
||||
const WORKER_EVENT_QUEUE_SIZE_WARNING_TRESHOLD: u64 = 3;
|
||||
// should be quick and provide good enough compression
|
||||
// if changed, update CACHE_CONFIGURATION.md
|
||||
const DEFAULT_BASELINE_COMPRESSION_LEVEL: i32 = zstd::DEFAULT_COMPRESSION_LEVEL;
|
||||
// should provide significantly better compression than baseline
|
||||
// if changed, update CACHE_CONFIGURATION.md
|
||||
const DEFAULT_OPTIMIZED_COMPRESSION_LEVEL: i32 = 20;
|
||||
// shouldn't be to low to avoid recompressing too many files
|
||||
// if changed, update CACHE_CONFIGURATION.md
|
||||
const DEFAULT_OPTIMIZED_COMPRESSION_USAGE_COUNTER_THRESHOLD: u64 = 0x100;
|
||||
// if changed, update CACHE_CONFIGURATION.md
|
||||
const DEFAULT_CLEANUP_INTERVAL: Duration = Duration::from_secs(60 * 60);
|
||||
// if changed, update CACHE_CONFIGURATION.md
|
||||
const DEFAULT_OPTIMIZING_COMPRESSION_TASK_TIMEOUT: Duration = Duration::from_secs(30 * 60);
|
||||
// the default assumes problems with timezone configuration on network share + some clock drift
|
||||
// please notice 24 timezones = max 23h difference between some of them
|
||||
// if changed, update CACHE_CONFIGURATION.md
|
||||
const DEFAULT_ALLOWED_CLOCK_DRIFT_FOR_FILES_FROM_FUTURE: Duration =
|
||||
Duration::from_secs(60 * 60 * 24);
|
||||
// if changed, update CACHE_CONFIGURATION.md
|
||||
const DEFAULT_FILE_COUNT_SOFT_LIMIT: u64 = 0x10_000;
|
||||
// if changed, update CACHE_CONFIGURATION.md
|
||||
const DEFAULT_FILES_TOTAL_SIZE_SOFT_LIMIT: u64 = 1024 * 1024 * 512;
|
||||
// if changed, update CACHE_CONFIGURATION.md
|
||||
const DEFAULT_FILE_COUNT_LIMIT_PERCENT_IF_DELETING: u8 = 70;
|
||||
// if changed, update CACHE_CONFIGURATION.md
|
||||
const DEFAULT_FILES_TOTAL_SIZE_LIMIT_PERCENT_IF_DELETING: u8 = 70;
|
||||
|
||||
// Deserializers of our custom formats
|
||||
// can be replaced with const generics later
|
||||
macro_rules! generate_deserializer {
|
||||
($name:ident($numname:ident: $numty:ty, $unitname:ident: &str) -> $retty:ty {$body:expr}) => {
|
||||
fn $name<'de, D>(deserializer: D) -> Result<$retty, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let text = Option::<String>::deserialize(deserializer)?;
|
||||
let text = match text {
|
||||
None => return Ok(None),
|
||||
Some(text) => text,
|
||||
};
|
||||
let text = text.trim();
|
||||
let split_point = text.find(|c: char| !c.is_numeric());
|
||||
let (num, unit) = split_point.map_or_else(|| (text, ""), |p| text.split_at(p));
|
||||
let deserialized = (|| {
|
||||
let $numname = num.parse::<$numty>().ok()?;
|
||||
let $unitname = unit.trim();
|
||||
$body
|
||||
})();
|
||||
if deserialized.is_some() {
|
||||
Ok(deserialized)
|
||||
} else {
|
||||
Err(de::Error::custom(
|
||||
"Invalid value, please refer to the documentation",
|
||||
))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
generate_deserializer!(deserialize_duration(num: u64, unit: &str) -> Option<Duration> {
|
||||
match unit {
|
||||
"s" => Some(Duration::from_secs(num)),
|
||||
"m" => Some(Duration::from_secs(num * 60)),
|
||||
"h" => Some(Duration::from_secs(num * 60 * 60)),
|
||||
"d" => Some(Duration::from_secs(num * 60 * 60 * 24)),
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
|
||||
generate_deserializer!(deserialize_si_prefix(num: u64, unit: &str) -> Option<u64> {
|
||||
match unit {
|
||||
"" => Some(num),
|
||||
"K" => num.checked_mul(1_000),
|
||||
"M" => num.checked_mul(1_000_000),
|
||||
"G" => num.checked_mul(1_000_000_000),
|
||||
"T" => num.checked_mul(1_000_000_000_000),
|
||||
"P" => num.checked_mul(1_000_000_000_000_000),
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
|
||||
generate_deserializer!(deserialize_disk_space(num: u64, unit: &str) -> Option<u64> {
|
||||
match unit {
|
||||
"" => Some(num),
|
||||
"K" => num.checked_mul(1_000),
|
||||
"Ki" => num.checked_mul(1u64 << 10),
|
||||
"M" => num.checked_mul(1_000_000),
|
||||
"Mi" => num.checked_mul(1u64 << 20),
|
||||
"G" => num.checked_mul(1_000_000_000),
|
||||
"Gi" => num.checked_mul(1u64 << 30),
|
||||
"T" => num.checked_mul(1_000_000_000_000),
|
||||
"Ti" => num.checked_mul(1u64 << 40),
|
||||
"P" => num.checked_mul(1_000_000_000_000_000),
|
||||
"Pi" => num.checked_mul(1u64 << 50),
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
|
||||
generate_deserializer!(deserialize_percent(num: u8, unit: &str) -> Option<u8> {
|
||||
match unit {
|
||||
"%" => Some(num),
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
|
||||
static CACHE_IMPROPER_CONFIG_ERROR_MSG: &str =
|
||||
"Cache system should be enabled and all settings must be validated or defaulted";
|
||||
|
||||
macro_rules! generate_setting_getter {
|
||||
($setting:ident: $setting_type:ty) => {
|
||||
/// Returns `$setting`.
|
||||
///
|
||||
/// Panics if the cache is disabled.
|
||||
pub fn $setting(&self) -> $setting_type {
|
||||
self
|
||||
.$setting
|
||||
.expect(CACHE_IMPROPER_CONFIG_ERROR_MSG)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl CacheConfig {
|
||||
generate_setting_getter!(worker_event_queue_size: u64);
|
||||
generate_setting_getter!(baseline_compression_level: i32);
|
||||
generate_setting_getter!(optimized_compression_level: i32);
|
||||
generate_setting_getter!(optimized_compression_usage_counter_threshold: u64);
|
||||
generate_setting_getter!(cleanup_interval: Duration);
|
||||
generate_setting_getter!(optimizing_compression_task_timeout: Duration);
|
||||
generate_setting_getter!(allowed_clock_drift_for_files_from_future: Duration);
|
||||
generate_setting_getter!(file_count_soft_limit: u64);
|
||||
generate_setting_getter!(files_total_size_soft_limit: u64);
|
||||
generate_setting_getter!(file_count_limit_percent_if_deleting: u8);
|
||||
generate_setting_getter!(files_total_size_limit_percent_if_deleting: u8);
|
||||
|
||||
/// Returns true if and only if the cache is enabled.
|
||||
pub fn enabled(&self) -> bool {
|
||||
self.enabled
|
||||
}
|
||||
|
||||
/// Returns path to the cache directory.
|
||||
///
|
||||
/// Panics if the cache is disabled.
|
||||
pub fn directory(&self) -> &PathBuf {
|
||||
self.directory
|
||||
.as_ref()
|
||||
.expect(CACHE_IMPROPER_CONFIG_ERROR_MSG)
|
||||
}
|
||||
|
||||
pub fn new_cache_disabled() -> Self {
|
||||
Self {
|
||||
errors: Vec::new(),
|
||||
enabled: false,
|
||||
directory: None,
|
||||
worker_event_queue_size: None,
|
||||
baseline_compression_level: None,
|
||||
optimized_compression_level: None,
|
||||
optimized_compression_usage_counter_threshold: None,
|
||||
cleanup_interval: None,
|
||||
optimizing_compression_task_timeout: None,
|
||||
allowed_clock_drift_for_files_from_future: None,
|
||||
file_count_soft_limit: None,
|
||||
files_total_size_soft_limit: None,
|
||||
file_count_limit_percent_if_deleting: None,
|
||||
files_total_size_limit_percent_if_deleting: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn new_cache_enabled_template() -> Self {
|
||||
let mut conf = Self::new_cache_disabled();
|
||||
conf.enabled = true;
|
||||
conf
|
||||
}
|
||||
|
||||
fn new_cache_with_errors(errors: Vec<String>) -> Self {
|
||||
let mut conf = Self::new_cache_disabled();
|
||||
conf.errors = errors;
|
||||
conf
|
||||
}
|
||||
|
||||
pub fn from_file<P: AsRef<Path>>(enabled: bool, config_file: Option<P>) -> Self {
|
||||
if !enabled {
|
||||
return Self::new_cache_disabled();
|
||||
}
|
||||
|
||||
let mut config = match Self::load_and_parse_file(config_file) {
|
||||
Ok(data) => data,
|
||||
Err(err) => return Self::new_cache_with_errors(vec![err]),
|
||||
};
|
||||
|
||||
// validate values and fill in defaults
|
||||
config.validate_directory_or_default();
|
||||
config.validate_worker_event_queue_size_or_default();
|
||||
config.validate_baseline_compression_level_or_default();
|
||||
config.validate_optimized_compression_level_or_default();
|
||||
config.validate_optimized_compression_usage_counter_threshold_or_default();
|
||||
config.validate_cleanup_interval_or_default();
|
||||
config.validate_optimizing_compression_task_timeout_or_default();
|
||||
config.validate_allowed_clock_drift_for_files_from_future_or_default();
|
||||
config.validate_file_count_soft_limit_or_default();
|
||||
config.validate_files_total_size_soft_limit_or_default();
|
||||
config.validate_file_count_limit_percent_if_deleting_or_default();
|
||||
config.validate_files_total_size_limit_percent_if_deleting_or_default();
|
||||
|
||||
config.disable_if_any_error();
|
||||
config
|
||||
}
|
||||
|
||||
fn load_and_parse_file<P: AsRef<Path>>(config_file: Option<P>) -> Result<Self, String> {
|
||||
// get config file path
|
||||
let (config_file, user_custom_file) = config_file.as_ref().map_or_else(
|
||||
|| DEFAULT_CONFIG_PATH.as_ref().map(|p| (p.as_ref(), false)),
|
||||
|p| Ok((p.as_ref(), true)),
|
||||
)?;
|
||||
|
||||
// read config, or use default one
|
||||
let entity_exists = config_file.exists();
|
||||
match (entity_exists, user_custom_file) {
|
||||
(false, false) => Ok(Self::new_cache_enabled_template()),
|
||||
_ => match fs::read(&config_file) {
|
||||
Ok(bytes) => match toml::from_slice::<Config>(&bytes[..]) {
|
||||
Ok(config) => Ok(config.cache),
|
||||
Err(err) => Err(format!(
|
||||
"Failed to parse config file, path: {}, error: {}",
|
||||
config_file.display(),
|
||||
err
|
||||
)),
|
||||
},
|
||||
Err(err) => Err(format!(
|
||||
"Failed to read config file, path: {}, error: {}",
|
||||
config_file.display(),
|
||||
err
|
||||
)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_directory_or_default(&mut self) {
|
||||
if self.directory.is_none() {
|
||||
match &*PROJECT_DIRS {
|
||||
Some(proj_dirs) => self.directory = Some(proj_dirs.cache_dir().to_path_buf()),
|
||||
None => {
|
||||
self.errors.push(
|
||||
"Cache directory not specified and failed to get the default".to_string(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// On Windows, if we want long paths, we need '\\?\' prefix, but it doesn't work
|
||||
// with relative paths. One way to get absolute path (the only one?) is to use
|
||||
// fs::canonicalize, but it requires that given path exists. The extra advantage
|
||||
// of this method is fact that the method prepends '\\?\' on Windows.
|
||||
let cache_dir = self.directory.as_ref().unwrap();
|
||||
|
||||
if !cache_dir.is_absolute() {
|
||||
self.errors.push(format!(
|
||||
"Cache directory path has to be absolute, path: {}",
|
||||
cache_dir.display(),
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
match fs::create_dir_all(cache_dir) {
|
||||
Ok(()) => (),
|
||||
Err(err) => {
|
||||
self.errors.push(format!(
|
||||
"Failed to create the cache directory, path: {}, error: {}",
|
||||
cache_dir.display(),
|
||||
err
|
||||
));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match fs::canonicalize(cache_dir) {
|
||||
Ok(p) => self.directory = Some(p),
|
||||
Err(err) => {
|
||||
self.errors.push(format!(
|
||||
"Failed to canonicalize the cache directory, path: {}, error: {}",
|
||||
cache_dir.display(),
|
||||
err
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_worker_event_queue_size_or_default(&mut self) {
|
||||
if self.worker_event_queue_size.is_none() {
|
||||
self.worker_event_queue_size = Some(DEFAULT_WORKER_EVENT_QUEUE_SIZE);
|
||||
}
|
||||
|
||||
if self.worker_event_queue_size.unwrap() < WORKER_EVENT_QUEUE_SIZE_WARNING_TRESHOLD {
|
||||
warn!("Detected small worker event queue size. Some messages might be lost.");
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_baseline_compression_level_or_default(&mut self) {
|
||||
if self.baseline_compression_level.is_none() {
|
||||
self.baseline_compression_level = Some(DEFAULT_BASELINE_COMPRESSION_LEVEL);
|
||||
}
|
||||
|
||||
if !ZSTD_COMPRESSION_LEVELS.contains(&self.baseline_compression_level.unwrap()) {
|
||||
self.errors.push(format!(
|
||||
"Invalid baseline compression level: {} not in {:#?}",
|
||||
self.baseline_compression_level.unwrap(),
|
||||
ZSTD_COMPRESSION_LEVELS
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// assumption: baseline compression level has been verified
|
||||
fn validate_optimized_compression_level_or_default(&mut self) {
|
||||
if self.optimized_compression_level.is_none() {
|
||||
self.optimized_compression_level = Some(DEFAULT_OPTIMIZED_COMPRESSION_LEVEL);
|
||||
}
|
||||
|
||||
let opt_lvl = self.optimized_compression_level.unwrap();
|
||||
let base_lvl = self.baseline_compression_level.unwrap();
|
||||
|
||||
if !ZSTD_COMPRESSION_LEVELS.contains(&opt_lvl) {
|
||||
self.errors.push(format!(
|
||||
"Invalid optimized compression level: {} not in {:#?}",
|
||||
opt_lvl, ZSTD_COMPRESSION_LEVELS
|
||||
));
|
||||
}
|
||||
|
||||
if opt_lvl < base_lvl {
|
||||
self.errors.push(format!(
|
||||
"Invalid optimized compression level is lower than baseline: {} < {}",
|
||||
opt_lvl, base_lvl
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_optimized_compression_usage_counter_threshold_or_default(&mut self) {
|
||||
if self.optimized_compression_usage_counter_threshold.is_none() {
|
||||
self.optimized_compression_usage_counter_threshold =
|
||||
Some(DEFAULT_OPTIMIZED_COMPRESSION_USAGE_COUNTER_THRESHOLD);
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_cleanup_interval_or_default(&mut self) {
|
||||
if self.cleanup_interval.is_none() {
|
||||
self.cleanup_interval = Some(DEFAULT_CLEANUP_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_optimizing_compression_task_timeout_or_default(&mut self) {
|
||||
if self.optimizing_compression_task_timeout.is_none() {
|
||||
self.optimizing_compression_task_timeout =
|
||||
Some(DEFAULT_OPTIMIZING_COMPRESSION_TASK_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_allowed_clock_drift_for_files_from_future_or_default(&mut self) {
|
||||
if self.allowed_clock_drift_for_files_from_future.is_none() {
|
||||
self.allowed_clock_drift_for_files_from_future =
|
||||
Some(DEFAULT_ALLOWED_CLOCK_DRIFT_FOR_FILES_FROM_FUTURE);
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_file_count_soft_limit_or_default(&mut self) {
|
||||
if self.file_count_soft_limit.is_none() {
|
||||
self.file_count_soft_limit = Some(DEFAULT_FILE_COUNT_SOFT_LIMIT);
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_files_total_size_soft_limit_or_default(&mut self) {
|
||||
if self.files_total_size_soft_limit.is_none() {
|
||||
self.files_total_size_soft_limit = Some(DEFAULT_FILES_TOTAL_SIZE_SOFT_LIMIT);
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_file_count_limit_percent_if_deleting_or_default(&mut self) {
|
||||
if self.file_count_limit_percent_if_deleting.is_none() {
|
||||
self.file_count_limit_percent_if_deleting =
|
||||
Some(DEFAULT_FILE_COUNT_LIMIT_PERCENT_IF_DELETING);
|
||||
}
|
||||
|
||||
let percent = self.file_count_limit_percent_if_deleting.unwrap();
|
||||
if percent > 100 {
|
||||
self.errors.push(format!(
|
||||
"Invalid files count limit percent if deleting: {} not in range 0-100%",
|
||||
percent
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_files_total_size_limit_percent_if_deleting_or_default(&mut self) {
|
||||
if self.files_total_size_limit_percent_if_deleting.is_none() {
|
||||
self.files_total_size_limit_percent_if_deleting =
|
||||
Some(DEFAULT_FILES_TOTAL_SIZE_LIMIT_PERCENT_IF_DELETING);
|
||||
}
|
||||
|
||||
let percent = self.files_total_size_limit_percent_if_deleting.unwrap();
|
||||
if percent > 100 {
|
||||
self.errors.push(format!(
|
||||
"Invalid files total size limit percent if deleting: {} not in range 0-100%",
|
||||
percent
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn disable_if_any_error(&mut self) {
|
||||
if !self.errors.is_empty() {
|
||||
let mut conf = Self::new_cache_disabled();
|
||||
mem::swap(self, &mut conf);
|
||||
mem::swap(&mut self.errors, &mut conf.errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
pub mod tests;
|
||||
581
crates/environ/src/cache/config/tests.rs
vendored
Normal file
581
crates/environ/src/cache/config/tests.rs
vendored
Normal file
@@ -0,0 +1,581 @@
|
||||
use super::CacheConfig;
|
||||
use core::time::Duration;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::{self, TempDir};
|
||||
|
||||
// note: config loading during validation creates cache directory to canonicalize its path,
|
||||
// that's why these function and macro always use custom cache directory
|
||||
// note: tempdir removes directory when being dropped, so we need to return it to the caller,
|
||||
// so the paths are valid
|
||||
pub fn test_prolog() -> (TempDir, PathBuf, PathBuf) {
|
||||
let _ = pretty_env_logger::try_init();
|
||||
let temp_dir = tempfile::tempdir().expect("Can't create temporary directory");
|
||||
let cache_dir = temp_dir.path().join("cache-dir");
|
||||
let config_path = temp_dir.path().join("cache-config.toml");
|
||||
(temp_dir, cache_dir, config_path)
|
||||
}
|
||||
|
||||
macro_rules! load_config {
|
||||
($config_path:ident, $content_fmt:expr, $cache_dir:ident) => {{
|
||||
let config_path = &$config_path;
|
||||
let content = format!(
|
||||
$content_fmt,
|
||||
cache_dir = toml::to_string_pretty(&format!("{}", $cache_dir.display())).unwrap()
|
||||
);
|
||||
fs::write(config_path, content).expect("Failed to write test config file");
|
||||
CacheConfig::from_file(true, Some(config_path))
|
||||
}};
|
||||
}
|
||||
|
||||
// test without macros to test being disabled
|
||||
#[test]
|
||||
fn test_disabled() {
|
||||
let dir = tempfile::tempdir().expect("Can't create temporary directory");
|
||||
let config_path = dir.path().join("cache-config.toml");
|
||||
let config_content = "[cache]\n\
|
||||
enabled = true\n";
|
||||
fs::write(&config_path, config_content).expect("Failed to write test config file");
|
||||
let conf = CacheConfig::from_file(false, Some(&config_path));
|
||||
assert!(!conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
|
||||
let config_content = "[cache]\n\
|
||||
enabled = false\n";
|
||||
fs::write(&config_path, config_content).expect("Failed to write test config file");
|
||||
let conf = CacheConfig::from_file(true, Some(&config_path));
|
||||
assert!(!conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unrecognized_settings() {
|
||||
let (_td, cd, cp) = test_prolog();
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"unrecognized-setting = 42\n\
|
||||
[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
unrecognized-setting = 42",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_settings() {
|
||||
let (_td, cd, cp) = test_prolog();
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
worker-event-queue-size = '16'\n\
|
||||
baseline-compression-level = 3\n\
|
||||
optimized-compression-level = 20\n\
|
||||
optimized-compression-usage-counter-threshold = '256'\n\
|
||||
cleanup-interval = '1h'\n\
|
||||
optimizing-compression-task-timeout = '30m'\n\
|
||||
allowed-clock-drift-for-files-from-future = '1d'\n\
|
||||
file-count-soft-limit = '65536'\n\
|
||||
files-total-size-soft-limit = '512Mi'\n\
|
||||
file-count-limit-percent-if-deleting = '70%'\n\
|
||||
files-total-size-limit-percent-if-deleting = '70%'",
|
||||
cd
|
||||
);
|
||||
check_conf(&conf, &cd);
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
// added some white spaces
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
worker-event-queue-size = ' 16\t'\n\
|
||||
baseline-compression-level = 3\n\
|
||||
optimized-compression-level =\t 20\n\
|
||||
optimized-compression-usage-counter-threshold = '256'\n\
|
||||
cleanup-interval = ' 1h'\n\
|
||||
optimizing-compression-task-timeout = '30 m'\n\
|
||||
allowed-clock-drift-for-files-from-future = '1\td'\n\
|
||||
file-count-soft-limit = '\t \t65536\t'\n\
|
||||
files-total-size-soft-limit = '512\t\t Mi '\n\
|
||||
file-count-limit-percent-if-deleting = '70\t%'\n\
|
||||
files-total-size-limit-percent-if-deleting = ' 70 %'",
|
||||
cd
|
||||
);
|
||||
check_conf(&conf, &cd);
|
||||
|
||||
fn check_conf(conf: &CacheConfig, cd: &PathBuf) {
|
||||
eprintln!("errors: {:#?}", conf.errors);
|
||||
assert!(conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
assert_eq!(
|
||||
conf.directory(),
|
||||
&fs::canonicalize(cd).expect("canonicalize failed")
|
||||
);
|
||||
assert_eq!(conf.worker_event_queue_size(), 0x10);
|
||||
assert_eq!(conf.baseline_compression_level(), 3);
|
||||
assert_eq!(conf.optimized_compression_level(), 20);
|
||||
assert_eq!(conf.optimized_compression_usage_counter_threshold(), 0x100);
|
||||
assert_eq!(conf.cleanup_interval(), Duration::from_secs(60 * 60));
|
||||
assert_eq!(
|
||||
conf.optimizing_compression_task_timeout(),
|
||||
Duration::from_secs(30 * 60)
|
||||
);
|
||||
assert_eq!(
|
||||
conf.allowed_clock_drift_for_files_from_future(),
|
||||
Duration::from_secs(60 * 60 * 24)
|
||||
);
|
||||
assert_eq!(conf.file_count_soft_limit(), 0x10_000);
|
||||
assert_eq!(conf.files_total_size_soft_limit(), 512 * (1u64 << 20));
|
||||
assert_eq!(conf.file_count_limit_percent_if_deleting(), 70);
|
||||
assert_eq!(conf.files_total_size_limit_percent_if_deleting(), 70);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compression_level_settings() {
|
||||
let (_td, cd, cp) = test_prolog();
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
baseline-compression-level = 1\n\
|
||||
optimized-compression-level = 21",
|
||||
cd
|
||||
);
|
||||
assert!(conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
assert_eq!(conf.baseline_compression_level(), 1);
|
||||
assert_eq!(conf.optimized_compression_level(), 21);
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
baseline-compression-level = -1\n\
|
||||
optimized-compression-level = 21",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
baseline-compression-level = 15\n\
|
||||
optimized-compression-level = 10",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_si_prefix_settings() {
|
||||
let (_td, cd, cp) = test_prolog();
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
worker-event-queue-size = '42'\n\
|
||||
optimized-compression-usage-counter-threshold = '4K'\n\
|
||||
file-count-soft-limit = '3M'",
|
||||
cd
|
||||
);
|
||||
assert!(conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
assert_eq!(conf.worker_event_queue_size(), 42);
|
||||
assert_eq!(conf.optimized_compression_usage_counter_threshold(), 4_000);
|
||||
assert_eq!(conf.file_count_soft_limit(), 3_000_000);
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
worker-event-queue-size = '2G'\n\
|
||||
optimized-compression-usage-counter-threshold = '4444T'\n\
|
||||
file-count-soft-limit = '1P'",
|
||||
cd
|
||||
);
|
||||
assert!(conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
assert_eq!(conf.worker_event_queue_size(), 2_000_000_000);
|
||||
assert_eq!(
|
||||
conf.optimized_compression_usage_counter_threshold(),
|
||||
4_444_000_000_000_000
|
||||
);
|
||||
assert_eq!(conf.file_count_soft_limit(), 1_000_000_000_000_000);
|
||||
|
||||
// different errors
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
worker-event-queue-size = '2g'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
file-count-soft-limit = 1",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
file-count-soft-limit = '-31337'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
file-count-soft-limit = '3.14M'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disk_space_settings() {
|
||||
let (_td, cd, cp) = test_prolog();
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-soft-limit = '76'",
|
||||
cd
|
||||
);
|
||||
assert!(conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
assert_eq!(conf.files_total_size_soft_limit(), 76);
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-soft-limit = '42 Mi'",
|
||||
cd
|
||||
);
|
||||
assert!(conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
assert_eq!(conf.files_total_size_soft_limit(), 42 * (1u64 << 20));
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-soft-limit = '2 Gi'",
|
||||
cd
|
||||
);
|
||||
assert!(conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
assert_eq!(conf.files_total_size_soft_limit(), 2 * (1u64 << 30));
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-soft-limit = '31337 Ti'",
|
||||
cd
|
||||
);
|
||||
assert!(conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
assert_eq!(conf.files_total_size_soft_limit(), 31337 * (1u64 << 40));
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-soft-limit = '7 Pi'",
|
||||
cd
|
||||
);
|
||||
assert!(conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
assert_eq!(conf.files_total_size_soft_limit(), 7 * (1u64 << 50));
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-soft-limit = '7M'",
|
||||
cd
|
||||
);
|
||||
assert!(conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
assert_eq!(conf.files_total_size_soft_limit(), 7_000_000);
|
||||
|
||||
// different errors
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-soft-limit = '7 mi'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-soft-limit = 1",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-soft-limit = '-31337'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-soft-limit = '3.14Ki'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_settings() {
|
||||
let (_td, cd, cp) = test_prolog();
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
cleanup-interval = '100s'\n\
|
||||
optimizing-compression-task-timeout = '3m'\n\
|
||||
allowed-clock-drift-for-files-from-future = '4h'",
|
||||
cd
|
||||
);
|
||||
assert!(conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
assert_eq!(conf.cleanup_interval(), Duration::from_secs(100));
|
||||
assert_eq!(
|
||||
conf.optimizing_compression_task_timeout(),
|
||||
Duration::from_secs(3 * 60)
|
||||
);
|
||||
assert_eq!(
|
||||
conf.allowed_clock_drift_for_files_from_future(),
|
||||
Duration::from_secs(4 * 60 * 60)
|
||||
);
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
cleanup-interval = '2d'\n\
|
||||
optimizing-compression-task-timeout = '333 m'",
|
||||
cd
|
||||
);
|
||||
assert!(conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
assert_eq!(
|
||||
conf.cleanup_interval(),
|
||||
Duration::from_secs(2 * 24 * 60 * 60)
|
||||
);
|
||||
assert_eq!(
|
||||
conf.optimizing_compression_task_timeout(),
|
||||
Duration::from_secs(333 * 60)
|
||||
);
|
||||
|
||||
// different errors
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
optimizing-compression-task-timeout = '333'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
optimizing-compression-task-timeout = 333",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
optimizing-compression-task-timeout = '10 M'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
optimizing-compression-task-timeout = '10 min'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
optimizing-compression-task-timeout = '-10s'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
optimizing-compression-task-timeout = '1.5m'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_percent_settings() {
|
||||
let (_td, cd, cp) = test_prolog();
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
file-count-limit-percent-if-deleting = '62%'\n\
|
||||
files-total-size-limit-percent-if-deleting = '23 %'",
|
||||
cd
|
||||
);
|
||||
assert!(conf.enabled());
|
||||
assert!(conf.errors.is_empty());
|
||||
assert_eq!(conf.file_count_limit_percent_if_deleting(), 62);
|
||||
assert_eq!(conf.files_total_size_limit_percent_if_deleting(), 23);
|
||||
|
||||
// different errors
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-limit-percent-if-deleting = '23'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-limit-percent-if-deleting = '22.5%'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-limit-percent-if-deleting = '0.5'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-limit-percent-if-deleting = '-1%'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
|
||||
let conf = load_config!(
|
||||
cp,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
files-total-size-limit-percent-if-deleting = '101%'",
|
||||
cd
|
||||
);
|
||||
assert!(!conf.enabled());
|
||||
assert!(!conf.errors.is_empty());
|
||||
}
|
||||
354
crates/environ/src/cache/tests.rs
vendored
Normal file
354
crates/environ/src/cache/tests.rs
vendored
Normal file
@@ -0,0 +1,354 @@
|
||||
use super::config::tests::test_prolog;
|
||||
use super::*;
|
||||
use crate::address_map::{FunctionAddressMap, InstructionAddressMap};
|
||||
use crate::compilation::{CompiledFunction, Relocation, RelocationTarget, TrapInformation};
|
||||
use crate::module::{MemoryPlan, MemoryStyle, Module};
|
||||
use alloc::boxed::Box;
|
||||
use alloc::vec::Vec;
|
||||
use core::cmp::min;
|
||||
use cranelift_codegen::{binemit, ir, isa, settings, ValueLocRange};
|
||||
use cranelift_entity::EntityRef;
|
||||
use cranelift_entity::{PrimaryMap, SecondaryMap};
|
||||
use cranelift_wasm::{DefinedFuncIndex, FuncIndex, Global, GlobalInit, Memory, SignatureIndex};
|
||||
use rand::rngs::SmallRng;
|
||||
use rand::{Rng, SeedableRng};
|
||||
use std::fs;
|
||||
use std::str::FromStr;
|
||||
use target_lexicon::triple;
|
||||
|
||||
// Since cache system is a global thing, each test needs to be run in seperate process.
|
||||
// So, init() tests are run as integration tests.
|
||||
// However, caching is a private thing, an implementation detail, and needs to be tested
|
||||
// from the inside of the module.
|
||||
// We test init() in exactly one test, rest of the tests doesn't rely on it.
|
||||
|
||||
#[test]
|
||||
fn test_cache_init() {
|
||||
let (_tempdir, cache_dir, config_path) = test_prolog();
|
||||
let baseline_compression_level = 4;
|
||||
let config_content = format!(
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {}\n\
|
||||
baseline-compression-level = {}\n",
|
||||
toml::to_string_pretty(&format!("{}", cache_dir.display())).unwrap(),
|
||||
baseline_compression_level,
|
||||
);
|
||||
fs::write(&config_path, config_content).expect("Failed to write test config file");
|
||||
|
||||
let errors = init(true, Some(&config_path), None);
|
||||
assert!(errors.is_empty());
|
||||
|
||||
// test if we can use config
|
||||
let cache_config = cache_config();
|
||||
assert!(cache_config.enabled());
|
||||
// assumption: config init creates cache directory and returns canonicalized path
|
||||
assert_eq!(
|
||||
*cache_config.directory(),
|
||||
fs::canonicalize(cache_dir).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
cache_config.baseline_compression_level(),
|
||||
baseline_compression_level
|
||||
);
|
||||
|
||||
// test if we can use worker
|
||||
let worker = worker();
|
||||
worker.on_cache_update_async(config_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_read_cache() {
|
||||
let (_tempdir, cache_dir, config_path) = test_prolog();
|
||||
let cache_config = load_config!(
|
||||
config_path,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
baseline-compression-level = 3\n",
|
||||
cache_dir
|
||||
);
|
||||
assert!(cache_config.enabled());
|
||||
let worker = Worker::start_new(&cache_config, None);
|
||||
|
||||
// assumption: config load creates cache directory and returns canonicalized path
|
||||
assert_eq!(
|
||||
*cache_config.directory(),
|
||||
fs::canonicalize(cache_dir).unwrap()
|
||||
);
|
||||
|
||||
let mut rng = SmallRng::from_seed([
|
||||
0x42, 0x04, 0xF3, 0x44, 0x11, 0x22, 0x33, 0x44, 0x67, 0x68, 0xFF, 0x00, 0x44, 0x23, 0x7F,
|
||||
0x96,
|
||||
]);
|
||||
|
||||
let mut code_container = Vec::new();
|
||||
code_container.resize(0x4000, 0);
|
||||
rng.fill(&mut code_container[..]);
|
||||
|
||||
let isa1 = new_isa("riscv64-unknown-unknown");
|
||||
let isa2 = new_isa("i386");
|
||||
let module1 = new_module(&mut rng);
|
||||
let module2 = new_module(&mut rng);
|
||||
let function_body_inputs1 = new_function_body_inputs(&mut rng, &code_container);
|
||||
let function_body_inputs2 = new_function_body_inputs(&mut rng, &code_container);
|
||||
let compiler1 = "test-1";
|
||||
let compiler2 = "test-2";
|
||||
|
||||
let entry1 = ModuleCacheEntry::from_inner(ModuleCacheEntryInner::new(
|
||||
&module1,
|
||||
&function_body_inputs1,
|
||||
&*isa1,
|
||||
compiler1,
|
||||
false,
|
||||
&cache_config,
|
||||
&worker,
|
||||
));
|
||||
assert!(entry1.0.is_some());
|
||||
assert!(entry1.get_data().is_none());
|
||||
let data1 = new_module_cache_data(&mut rng);
|
||||
entry1.update_data(&data1);
|
||||
assert_eq!(entry1.get_data().expect("Cache should be available"), data1);
|
||||
|
||||
let entry2 = ModuleCacheEntry::from_inner(ModuleCacheEntryInner::new(
|
||||
&module2,
|
||||
&function_body_inputs1,
|
||||
&*isa1,
|
||||
compiler1,
|
||||
false,
|
||||
&cache_config,
|
||||
&worker,
|
||||
));
|
||||
let data2 = new_module_cache_data(&mut rng);
|
||||
entry2.update_data(&data2);
|
||||
assert_eq!(entry1.get_data().expect("Cache should be available"), data1);
|
||||
assert_eq!(entry2.get_data().expect("Cache should be available"), data2);
|
||||
|
||||
let entry3 = ModuleCacheEntry::from_inner(ModuleCacheEntryInner::new(
|
||||
&module1,
|
||||
&function_body_inputs2,
|
||||
&*isa1,
|
||||
compiler1,
|
||||
false,
|
||||
&cache_config,
|
||||
&worker,
|
||||
));
|
||||
let data3 = new_module_cache_data(&mut rng);
|
||||
entry3.update_data(&data3);
|
||||
assert_eq!(entry1.get_data().expect("Cache should be available"), data1);
|
||||
assert_eq!(entry2.get_data().expect("Cache should be available"), data2);
|
||||
assert_eq!(entry3.get_data().expect("Cache should be available"), data3);
|
||||
|
||||
let entry4 = ModuleCacheEntry::from_inner(ModuleCacheEntryInner::new(
|
||||
&module1,
|
||||
&function_body_inputs1,
|
||||
&*isa2,
|
||||
compiler1,
|
||||
false,
|
||||
&cache_config,
|
||||
&worker,
|
||||
));
|
||||
let data4 = new_module_cache_data(&mut rng);
|
||||
entry4.update_data(&data4);
|
||||
assert_eq!(entry1.get_data().expect("Cache should be available"), data1);
|
||||
assert_eq!(entry2.get_data().expect("Cache should be available"), data2);
|
||||
assert_eq!(entry3.get_data().expect("Cache should be available"), data3);
|
||||
assert_eq!(entry4.get_data().expect("Cache should be available"), data4);
|
||||
|
||||
let entry5 = ModuleCacheEntry::from_inner(ModuleCacheEntryInner::new(
|
||||
&module1,
|
||||
&function_body_inputs1,
|
||||
&*isa1,
|
||||
compiler2,
|
||||
false,
|
||||
&cache_config,
|
||||
&worker,
|
||||
));
|
||||
let data5 = new_module_cache_data(&mut rng);
|
||||
entry5.update_data(&data5);
|
||||
assert_eq!(entry1.get_data().expect("Cache should be available"), data1);
|
||||
assert_eq!(entry2.get_data().expect("Cache should be available"), data2);
|
||||
assert_eq!(entry3.get_data().expect("Cache should be available"), data3);
|
||||
assert_eq!(entry4.get_data().expect("Cache should be available"), data4);
|
||||
assert_eq!(entry5.get_data().expect("Cache should be available"), data5);
|
||||
|
||||
let data6 = new_module_cache_data(&mut rng);
|
||||
entry1.update_data(&data6);
|
||||
assert_eq!(entry1.get_data().expect("Cache should be available"), data6);
|
||||
assert_eq!(entry2.get_data().expect("Cache should be available"), data2);
|
||||
assert_eq!(entry3.get_data().expect("Cache should be available"), data3);
|
||||
assert_eq!(entry4.get_data().expect("Cache should be available"), data4);
|
||||
assert_eq!(entry5.get_data().expect("Cache should be available"), data5);
|
||||
|
||||
assert!(data1 != data2 && data1 != data3 && data1 != data4 && data1 != data5 && data1 != data6);
|
||||
}
|
||||
|
||||
fn new_isa(name: &str) -> Box<dyn isa::TargetIsa> {
|
||||
let shared_builder = settings::builder();
|
||||
let shared_flags = settings::Flags::new(shared_builder);
|
||||
isa::lookup(triple!(name))
|
||||
.expect("can't find specified isa")
|
||||
.finish(shared_flags)
|
||||
}
|
||||
|
||||
fn new_module(rng: &mut impl Rng) -> Module {
|
||||
// There are way too many fields. Just fill in some of them.
|
||||
let mut m = Module::new();
|
||||
|
||||
if rng.gen_bool(0.5) {
|
||||
m.signatures.push(ir::Signature {
|
||||
params: vec![],
|
||||
returns: vec![],
|
||||
call_conv: isa::CallConv::Fast,
|
||||
});
|
||||
}
|
||||
|
||||
for i in 0..rng.gen_range(1, 0x8) {
|
||||
m.functions.push(SignatureIndex::new(i));
|
||||
}
|
||||
|
||||
if rng.gen_bool(0.8) {
|
||||
m.memory_plans.push(MemoryPlan {
|
||||
memory: Memory {
|
||||
minimum: rng.gen(),
|
||||
maximum: rng.gen(),
|
||||
shared: rng.gen(),
|
||||
},
|
||||
style: MemoryStyle::Dynamic,
|
||||
offset_guard_size: rng.gen(),
|
||||
});
|
||||
}
|
||||
|
||||
if rng.gen_bool(0.4) {
|
||||
m.globals.push(Global {
|
||||
ty: ir::Type::int(16).unwrap(),
|
||||
mutability: rng.gen(),
|
||||
initializer: GlobalInit::I32Const(rng.gen()),
|
||||
});
|
||||
}
|
||||
|
||||
m
|
||||
}
|
||||
|
||||
fn new_function_body_inputs<'data>(
|
||||
rng: &mut impl Rng,
|
||||
code_container: &'data Vec<u8>,
|
||||
) -> PrimaryMap<DefinedFuncIndex, FunctionBodyData<'data>> {
|
||||
let len = code_container.len();
|
||||
let mut pos = rng.gen_range(0, code_container.len());
|
||||
(2..rng.gen_range(4, 14))
|
||||
.map(|j| {
|
||||
let (old_pos, end) = (pos, min(pos + rng.gen_range(0x10, 0x200), len));
|
||||
pos = end % len;
|
||||
FunctionBodyData {
|
||||
data: &code_container[old_pos..end],
|
||||
module_offset: (rng.next_u64() + j) as usize,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn new_module_cache_data(rng: &mut impl Rng) -> ModuleCacheData {
|
||||
let funcs = (0..rng.gen_range(0, 10))
|
||||
.map(|i| {
|
||||
let mut sm = SecondaryMap::new(); // doesn't implement from iterator
|
||||
sm.resize(i as usize * 2);
|
||||
sm.values_mut().enumerate().for_each(|(j, v)| {
|
||||
if rng.gen_bool(0.33) {
|
||||
*v = (j as u32) * 3 / 4
|
||||
}
|
||||
});
|
||||
CompiledFunction {
|
||||
body: (0..(i * 3 / 2)).collect(),
|
||||
jt_offsets: sm,
|
||||
unwind_info: (0..(i * 3 / 2)).collect(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let relocs = (0..rng.gen_range(1, 0x10))
|
||||
.map(|i| {
|
||||
vec![
|
||||
Relocation {
|
||||
reloc: binemit::Reloc::X86CallPCRel4,
|
||||
reloc_target: RelocationTarget::UserFunc(FuncIndex::new(i as usize * 42)),
|
||||
offset: i + rng.next_u32(),
|
||||
addend: 0,
|
||||
},
|
||||
Relocation {
|
||||
reloc: binemit::Reloc::Arm32Call,
|
||||
reloc_target: RelocationTarget::LibCall(ir::LibCall::CeilF64),
|
||||
offset: rng.gen_range(4, i + 55),
|
||||
addend: (42 * i) as i64,
|
||||
},
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
|
||||
let trans = (4..rng.gen_range(4, 0x10))
|
||||
.map(|i| FunctionAddressMap {
|
||||
instructions: vec![InstructionAddressMap {
|
||||
srcloc: ir::SourceLoc::new(rng.gen()),
|
||||
code_offset: rng.gen(),
|
||||
code_len: i,
|
||||
}],
|
||||
start_srcloc: ir::SourceLoc::new(rng.gen()),
|
||||
end_srcloc: ir::SourceLoc::new(rng.gen()),
|
||||
body_offset: rng.gen(),
|
||||
body_len: 0x31337,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let value_ranges = (4..rng.gen_range(4, 0x10))
|
||||
.map(|i| {
|
||||
(i..i + rng.gen_range(4, 8))
|
||||
.map(|k| {
|
||||
(
|
||||
ir::ValueLabel::new(k),
|
||||
(0..rng.gen_range(0, 4))
|
||||
.map(|_| ValueLocRange {
|
||||
loc: ir::ValueLoc::Reg(rng.gen()),
|
||||
start: rng.gen(),
|
||||
end: rng.gen(),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let stack_slots = (0..rng.gen_range(0, 0x6))
|
||||
.map(|_| {
|
||||
let mut slots = ir::StackSlots::new();
|
||||
slots.push(ir::StackSlotData {
|
||||
kind: ir::StackSlotKind::SpillSlot,
|
||||
size: rng.gen(),
|
||||
offset: rng.gen(),
|
||||
});
|
||||
slots.frame_size = rng.gen();
|
||||
slots
|
||||
})
|
||||
.collect();
|
||||
|
||||
let traps = (0..rng.gen_range(0, 0xd))
|
||||
.map(|i| {
|
||||
((i..i + rng.gen_range(0, 4))
|
||||
.map(|_| TrapInformation {
|
||||
code_offset: rng.gen(),
|
||||
source_loc: ir::SourceLoc::new(rng.gen()),
|
||||
trap_code: ir::TrapCode::StackOverflow,
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
.collect();
|
||||
|
||||
ModuleCacheData::from_tuple((
|
||||
Compilation::new(funcs),
|
||||
relocs,
|
||||
trans,
|
||||
value_ranges,
|
||||
stack_slots,
|
||||
traps,
|
||||
))
|
||||
}
|
||||
912
crates/environ/src/cache/worker.rs
vendored
Normal file
912
crates/environ/src/cache/worker.rs
vendored
Normal file
@@ -0,0 +1,912 @@
|
||||
//! Background worker that watches over the cache.
|
||||
//!
|
||||
//! It cleans up old cache, updates statistics and optimizes the cache.
|
||||
//! We allow losing some messages (it doesn't hurt) and some races,
|
||||
//! but we guarantee eventual consistency and fault tolerancy.
|
||||
//! Background tasks can be CPU intensive, but the worker thread has low priority.
|
||||
|
||||
use super::{cache_config, fs_write_atomic, CacheConfig};
|
||||
use alloc::vec::Vec;
|
||||
use core::cmp;
|
||||
use core::time::Duration;
|
||||
use log::{debug, info, trace, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use spin::Once;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsStr;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{self, AtomicBool};
|
||||
use std::sync::mpsc::{sync_channel, Receiver, SyncSender};
|
||||
#[cfg(test)]
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
use std::thread;
|
||||
#[cfg(not(test))]
|
||||
use std::time::SystemTime;
|
||||
#[cfg(test)]
|
||||
use tests::system_time_stub::SystemTimeStub as SystemTime;
|
||||
|
||||
pub(super) struct Worker {
|
||||
sender: SyncSender<CacheEvent>,
|
||||
#[cfg(test)]
|
||||
stats: Arc<(Mutex<WorkerStats>, Condvar)>,
|
||||
}
|
||||
|
||||
struct WorkerThread {
|
||||
receiver: Receiver<CacheEvent>,
|
||||
cache_config: CacheConfig,
|
||||
#[cfg(test)]
|
||||
stats: Arc<(Mutex<WorkerStats>, Condvar)>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[derive(Default)]
|
||||
struct WorkerStats {
|
||||
dropped: u32,
|
||||
sent: u32,
|
||||
handled: u32,
|
||||
}
|
||||
|
||||
static WORKER: Once<Worker> = Once::new();
|
||||
static INIT_CALLED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub(super) fn worker() -> &'static Worker {
|
||||
WORKER
|
||||
.r#try()
|
||||
.expect("Cache worker must be initialized before usage")
|
||||
}
|
||||
|
||||
pub(super) fn init(init_file_per_thread_logger: Option<&'static str>) {
|
||||
INIT_CALLED
|
||||
.compare_exchange(
|
||||
false,
|
||||
true,
|
||||
atomic::Ordering::SeqCst,
|
||||
atomic::Ordering::SeqCst,
|
||||
)
|
||||
.expect("Cache worker init must be called at most once");
|
||||
|
||||
let worker = Worker::start_new(cache_config(), init_file_per_thread_logger);
|
||||
WORKER.call_once(|| worker);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum CacheEvent {
|
||||
OnCacheGet(PathBuf),
|
||||
OnCacheUpdate(PathBuf),
|
||||
}
|
||||
|
||||
impl Worker {
|
||||
pub(super) fn start_new(
|
||||
cache_config: &CacheConfig,
|
||||
init_file_per_thread_logger: Option<&'static str>,
|
||||
) -> Self {
|
||||
let queue_size = match cache_config.worker_event_queue_size() {
|
||||
num if num <= usize::max_value() as u64 => num as usize,
|
||||
_ => usize::max_value(),
|
||||
};
|
||||
let (tx, rx) = sync_channel(queue_size);
|
||||
|
||||
#[cfg(test)]
|
||||
let stats = Arc::new((Mutex::new(WorkerStats::default()), Condvar::new()));
|
||||
|
||||
let worker_thread = WorkerThread {
|
||||
receiver: rx,
|
||||
cache_config: cache_config.clone(),
|
||||
#[cfg(test)]
|
||||
stats: stats.clone(),
|
||||
};
|
||||
|
||||
// when self is dropped, sender will be dropped, what will cause the channel
|
||||
// to hang, and the worker thread to exit -- it happens in the tests
|
||||
// non-tests binary has only a static worker, so Rust doesn't drop it
|
||||
thread::spawn(move || worker_thread.run(init_file_per_thread_logger));
|
||||
|
||||
Self {
|
||||
sender: tx,
|
||||
#[cfg(test)]
|
||||
stats,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn on_cache_get_async(&self, path: impl AsRef<Path>) {
|
||||
let event = CacheEvent::OnCacheGet(path.as_ref().to_path_buf());
|
||||
self.send_cache_event(event);
|
||||
}
|
||||
|
||||
pub(super) fn on_cache_update_async(&self, path: impl AsRef<Path>) {
|
||||
let event = CacheEvent::OnCacheUpdate(path.as_ref().to_path_buf());
|
||||
self.send_cache_event(event);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn send_cache_event(&self, event: CacheEvent) {
|
||||
#[cfg(test)]
|
||||
let mut stats = self
|
||||
.stats
|
||||
.0
|
||||
.lock()
|
||||
.expect("Failed to acquire worker stats lock");
|
||||
match self.sender.try_send(event.clone()) {
|
||||
Ok(()) => {
|
||||
#[cfg(test)]
|
||||
let _ = stats.sent += 1;
|
||||
}
|
||||
Err(err) => {
|
||||
info!(
|
||||
"Failed to send asynchronously message to worker thread, \
|
||||
event: {:?}, error: {}",
|
||||
event, err
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
let _ = stats.dropped += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn events_dropped(&self) -> u32 {
|
||||
let stats = self
|
||||
.stats
|
||||
.0
|
||||
.lock()
|
||||
.expect("Failed to acquire worker stats lock");
|
||||
stats.dropped
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn wait_for_all_events_handled(&self) {
|
||||
let (stats, condvar) = &*self.stats;
|
||||
let mut stats = stats.lock().expect("Failed to acquire worker stats lock");
|
||||
while stats.handled != stats.sent {
|
||||
stats = condvar
|
||||
.wait(stats)
|
||||
.expect("Failed to reacquire worker stats lock");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct ModuleCacheStatistics {
|
||||
pub usages: u64,
|
||||
#[serde(rename = "optimized-compression")]
|
||||
pub compression_level: i32,
|
||||
}
|
||||
|
||||
impl ModuleCacheStatistics {
|
||||
fn default(cache_config: &CacheConfig) -> Self {
|
||||
Self {
|
||||
usages: 0,
|
||||
compression_level: cache_config.baseline_compression_level(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum CacheEntry {
|
||||
Recognized {
|
||||
path: PathBuf,
|
||||
mtime: SystemTime,
|
||||
size: u64,
|
||||
},
|
||||
Unrecognized {
|
||||
path: PathBuf,
|
||||
is_dir: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl WorkerThread {
|
||||
fn run(self, init_file_per_thread_logger: Option<&'static str>) {
|
||||
#[cfg(not(test))] // We want to test the worker without relying on init() being called
|
||||
assert!(INIT_CALLED.load(atomic::Ordering::SeqCst));
|
||||
|
||||
if let Some(prefix) = init_file_per_thread_logger {
|
||||
file_per_thread_logger::initialize(prefix);
|
||||
}
|
||||
|
||||
debug!("Cache worker thread started.");
|
||||
|
||||
Self::lower_thread_priority();
|
||||
|
||||
#[cfg(test)]
|
||||
let (stats, condvar) = &*self.stats;
|
||||
|
||||
for event in self.receiver.iter() {
|
||||
match event {
|
||||
CacheEvent::OnCacheGet(path) => self.handle_on_cache_get(path),
|
||||
CacheEvent::OnCacheUpdate(path) => self.handle_on_cache_update(path),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
{
|
||||
let mut stats = stats.lock().expect("Failed to acquire worker stats lock");
|
||||
stats.handled += 1;
|
||||
condvar.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
// The receiver can stop iteration iff the channel has hung up.
|
||||
// The channel will hung when sender is dropped. It only happens in tests.
|
||||
// In non-test case we have static worker and Rust doesn't drop static variables.
|
||||
#[cfg(not(test))]
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn lower_thread_priority() {
|
||||
use core::convert::TryInto;
|
||||
use winapi::um::processthreadsapi::{GetCurrentThread, SetThreadPriority};
|
||||
use winapi::um::winbase::THREAD_MODE_BACKGROUND_BEGIN;
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setthreadpriority
|
||||
// https://docs.microsoft.com/en-us/windows/win32/procthread/scheduling-priorities
|
||||
|
||||
if unsafe {
|
||||
SetThreadPriority(
|
||||
GetCurrentThread(),
|
||||
THREAD_MODE_BACKGROUND_BEGIN.try_into().unwrap(),
|
||||
)
|
||||
} == 0
|
||||
{
|
||||
warn!(
|
||||
"Failed to lower worker thread priority. It might affect application performance."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn lower_thread_priority() {
|
||||
// http://man7.org/linux/man-pages/man7/sched.7.html
|
||||
|
||||
const NICE_DELTA_FOR_BACKGROUND_TASKS: i32 = 3;
|
||||
|
||||
errno::set_errno(errno::Errno(0));
|
||||
let current_nice = unsafe { libc::nice(NICE_DELTA_FOR_BACKGROUND_TASKS) };
|
||||
let errno_val = errno::errno().0;
|
||||
|
||||
if errno_val != 0 {
|
||||
warn!("Failed to lower worker thread priority. It might affect application performance. errno: {}", errno_val);
|
||||
} else {
|
||||
debug!("New nice value of worker thread: {}", current_nice);
|
||||
}
|
||||
}
|
||||
|
||||
/// Increases the usage counter and recompresses the file
|
||||
/// if the usage counter reached configurable treshold.
|
||||
fn handle_on_cache_get(&self, path: PathBuf) {
|
||||
trace!("handle_on_cache_get() for path: {}", path.display());
|
||||
|
||||
// construct .stats file path
|
||||
let filename = path.file_name().unwrap().to_str().unwrap();
|
||||
let stats_path = path.with_file_name(format!("{}.stats", filename));
|
||||
|
||||
// load .stats file (default if none or error)
|
||||
let mut stats = read_stats_file(stats_path.as_ref())
|
||||
.unwrap_or_else(|| ModuleCacheStatistics::default(&self.cache_config));
|
||||
|
||||
// step 1: update the usage counter & write to the disk
|
||||
// it's racy, but it's fine (the counter will be just smaller,
|
||||
// sometimes will retrigger recompression)
|
||||
stats.usages += 1;
|
||||
if !write_stats_file(stats_path.as_ref(), &stats) {
|
||||
return;
|
||||
}
|
||||
|
||||
// step 2: recompress if there's a need
|
||||
let opt_compr_lvl = self.cache_config.optimized_compression_level();
|
||||
if stats.compression_level >= opt_compr_lvl
|
||||
|| stats.usages
|
||||
< self
|
||||
.cache_config
|
||||
.optimized_compression_usage_counter_threshold()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let lock_path = if let Some(p) = acquire_task_fs_lock(
|
||||
path.as_ref(),
|
||||
self.cache_config.optimizing_compression_task_timeout(),
|
||||
self.cache_config
|
||||
.allowed_clock_drift_for_files_from_future(),
|
||||
) {
|
||||
p
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
trace!("Trying to recompress file: {}", path.display());
|
||||
|
||||
// recompress, write to other file, rename (it's atomic file content exchange)
|
||||
// and update the stats file
|
||||
fs::read(&path)
|
||||
.map_err(|err| {
|
||||
warn!(
|
||||
"Failed to read old cache file, path: {}, err: {}",
|
||||
path.display(),
|
||||
err
|
||||
)
|
||||
})
|
||||
.ok()
|
||||
.and_then(|compressed_cache_bytes| {
|
||||
zstd::decode_all(&compressed_cache_bytes[..])
|
||||
.map_err(|err| warn!("Failed to decompress cached code: {}", err))
|
||||
.ok()
|
||||
})
|
||||
.and_then(|cache_bytes| {
|
||||
zstd::encode_all(
|
||||
&cache_bytes[..],
|
||||
opt_compr_lvl,
|
||||
)
|
||||
.map_err(|err| warn!("Failed to compress cached code: {}", err))
|
||||
.ok()
|
||||
})
|
||||
.and_then(|recompressed_cache_bytes| {
|
||||
fs::write(&lock_path, &recompressed_cache_bytes)
|
||||
.map_err(|err| {
|
||||
warn!(
|
||||
"Failed to write recompressed cache, path: {}, err: {}",
|
||||
lock_path.display(),
|
||||
err
|
||||
)
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.and_then(|()| {
|
||||
fs::rename(&lock_path, &path)
|
||||
.map_err(|err| {
|
||||
warn!(
|
||||
"Failed to rename recompressed cache, path from: {}, path to: {}, err: {}",
|
||||
lock_path.display(),
|
||||
path.display(),
|
||||
err
|
||||
);
|
||||
if let Err(err) = fs::remove_file(&lock_path) {
|
||||
warn!(
|
||||
"Failed to clean up (remove) recompressed cache, path {}, err: {}",
|
||||
lock_path.display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.map(|()| {
|
||||
// update stats file (reload it! recompression can take some time)
|
||||
if let Some(mut new_stats) = read_stats_file(stats_path.as_ref()) {
|
||||
if new_stats.compression_level >= opt_compr_lvl {
|
||||
// Rare race:
|
||||
// two instances with different opt_compr_lvl: we don't know in which order they updated
|
||||
// the cache file and the stats file (they are not updated together atomically)
|
||||
// Possible solution is to use directories per cache entry, but it complicates the system
|
||||
// and is not worth it.
|
||||
debug!("DETECTED task did more than once (or race with new file): recompression of {}. \
|
||||
Note: if optimized compression level setting has changed in the meantine, \
|
||||
the stats file might contain inconsistent compression level due to race.", path.display());
|
||||
}
|
||||
else {
|
||||
new_stats.compression_level = opt_compr_lvl;
|
||||
let _ = write_stats_file(stats_path.as_ref(), &new_stats);
|
||||
}
|
||||
|
||||
if new_stats.usages < stats.usages {
|
||||
debug!("DETECTED lower usage count (new file or race with counter increasing): file {}", path.display());
|
||||
}
|
||||
}
|
||||
else {
|
||||
debug!("Can't read stats file again to update compression level (it might got cleaned up): file {}", stats_path.display());
|
||||
}
|
||||
});
|
||||
|
||||
trace!("Task finished: recompress file: {}", path.display());
|
||||
}
|
||||
|
||||
fn handle_on_cache_update(&self, path: PathBuf) {
|
||||
trace!("handle_on_cache_update() for path: {}", path.display());
|
||||
|
||||
// ---------------------- step 1: create .stats file
|
||||
|
||||
// construct .stats file path
|
||||
let filename = path
|
||||
.file_name()
|
||||
.expect("Expected valid cache file name")
|
||||
.to_str()
|
||||
.expect("Expected valid cache file name");
|
||||
let stats_path = path.with_file_name(format!("{}.stats", filename));
|
||||
|
||||
// create and write stats file
|
||||
let mut stats = ModuleCacheStatistics::default(&self.cache_config);
|
||||
stats.usages += 1;
|
||||
write_stats_file(&stats_path, &stats);
|
||||
|
||||
// ---------------------- step 2: perform cleanup task if needed
|
||||
|
||||
// acquire lock for cleanup task
|
||||
// Lock is a proof of recent cleanup task, so we don't want to delete them.
|
||||
// Expired locks will be deleted by the cleanup task.
|
||||
let cleanup_file = self.cache_config.directory().join(".cleanup"); // some non existing marker file
|
||||
if acquire_task_fs_lock(
|
||||
&cleanup_file,
|
||||
self.cache_config.cleanup_interval(),
|
||||
self.cache_config
|
||||
.allowed_clock_drift_for_files_from_future(),
|
||||
)
|
||||
.is_none()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
trace!("Trying to clean up cache");
|
||||
|
||||
let mut cache_index = self.list_cache_contents();
|
||||
let future_tolerance = SystemTime::now()
|
||||
.checked_add(
|
||||
self.cache_config
|
||||
.allowed_clock_drift_for_files_from_future(),
|
||||
)
|
||||
.expect("Brace your cache, the next Big Bang is coming (time overflow)");
|
||||
cache_index.sort_unstable_by(|lhs, rhs| {
|
||||
// sort by age
|
||||
use CacheEntry::*;
|
||||
match (lhs, rhs) {
|
||||
(Recognized { mtime: lhs_mt, .. }, Recognized { mtime: rhs_mt, .. }) => {
|
||||
match (*lhs_mt > future_tolerance, *rhs_mt > future_tolerance) {
|
||||
// later == younger
|
||||
(false, false) => rhs_mt.cmp(lhs_mt),
|
||||
// files from far future are treated as oldest recognized files
|
||||
// we want to delete them, so the cache keeps track of recent files
|
||||
// however, we don't delete them uncodintionally,
|
||||
// because .stats file can be overwritten with a meaningful mtime
|
||||
(true, false) => cmp::Ordering::Greater,
|
||||
(false, true) => cmp::Ordering::Less,
|
||||
(true, true) => cmp::Ordering::Equal,
|
||||
}
|
||||
}
|
||||
// unrecognized is kind of infinity
|
||||
(Recognized { .. }, Unrecognized { .. }) => cmp::Ordering::Less,
|
||||
(Unrecognized { .. }, Recognized { .. }) => cmp::Ordering::Greater,
|
||||
(Unrecognized { .. }, Unrecognized { .. }) => cmp::Ordering::Equal,
|
||||
}
|
||||
});
|
||||
|
||||
// find "cut" boundary:
|
||||
// - remove unrecognized files anyway,
|
||||
// - remove some cache files if some quota has been exceeded
|
||||
let mut total_size = 0u64;
|
||||
let mut start_delete_idx = None;
|
||||
let mut start_delete_idx_if_deleting_recognized_items: Option<usize> = None;
|
||||
|
||||
let total_size_limit = self.cache_config.files_total_size_soft_limit();
|
||||
let file_count_limit = self.cache_config.file_count_soft_limit();
|
||||
let tsl_if_deleting = total_size_limit
|
||||
.checked_mul(
|
||||
self.cache_config
|
||||
.files_total_size_limit_percent_if_deleting() as u64,
|
||||
)
|
||||
.unwrap()
|
||||
/ 100;
|
||||
let fcl_if_deleting = file_count_limit
|
||||
.checked_mul(self.cache_config.file_count_limit_percent_if_deleting() as u64)
|
||||
.unwrap()
|
||||
/ 100;
|
||||
|
||||
for (idx, item) in cache_index.iter().enumerate() {
|
||||
let size = if let CacheEntry::Recognized { size, .. } = item {
|
||||
size
|
||||
} else {
|
||||
start_delete_idx = Some(idx);
|
||||
break;
|
||||
};
|
||||
|
||||
total_size += size;
|
||||
if start_delete_idx_if_deleting_recognized_items.is_none() {
|
||||
if total_size > tsl_if_deleting || (idx + 1) as u64 > fcl_if_deleting {
|
||||
start_delete_idx_if_deleting_recognized_items = Some(idx);
|
||||
}
|
||||
}
|
||||
|
||||
if total_size > total_size_limit || (idx + 1) as u64 > file_count_limit {
|
||||
start_delete_idx = start_delete_idx_if_deleting_recognized_items;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(idx) = start_delete_idx {
|
||||
for item in &cache_index[idx..] {
|
||||
let (result, path, entity) = match item {
|
||||
CacheEntry::Recognized { path, .. }
|
||||
| CacheEntry::Unrecognized {
|
||||
path,
|
||||
is_dir: false,
|
||||
} => (fs::remove_file(path), path, "file"),
|
||||
CacheEntry::Unrecognized { path, is_dir: true } => {
|
||||
(fs::remove_dir_all(path), path, "directory")
|
||||
}
|
||||
};
|
||||
if let Err(err) = result {
|
||||
warn!(
|
||||
"Failed to remove {} during cleanup, path: {}, err: {}",
|
||||
entity,
|
||||
path.display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trace!("Task finished: clean up cache");
|
||||
}
|
||||
|
||||
// Be fault tolerant: list as much as you can, and ignore the rest
|
||||
fn list_cache_contents(&self) -> Vec<CacheEntry> {
|
||||
fn enter_dir(
|
||||
vec: &mut Vec<CacheEntry>,
|
||||
dir_path: &Path,
|
||||
level: u8,
|
||||
cache_config: &CacheConfig,
|
||||
) {
|
||||
macro_rules! unwrap_or {
|
||||
($result:expr, $cont:stmt, $err_msg:expr) => {
|
||||
unwrap_or!($result, $cont, $err_msg, dir_path)
|
||||
};
|
||||
($result:expr, $cont:stmt, $err_msg:expr, $path:expr) => {
|
||||
match $result {
|
||||
Ok(val) => val,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"{}, level: {}, path: {}, msg: {}",
|
||||
$err_msg,
|
||||
level,
|
||||
$path.display(),
|
||||
err
|
||||
);
|
||||
$cont
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
macro_rules! add_unrecognized {
|
||||
(file: $path:expr) => {
|
||||
add_unrecognized!(false, $path)
|
||||
};
|
||||
(dir: $path:expr) => {
|
||||
add_unrecognized!(true, $path)
|
||||
};
|
||||
($is_dir:expr, $path:expr) => {
|
||||
vec.push(CacheEntry::Unrecognized {
|
||||
path: $path.to_path_buf(),
|
||||
is_dir: $is_dir,
|
||||
});
|
||||
};
|
||||
}
|
||||
macro_rules! add_unrecognized_and {
|
||||
([ $( $ty:ident: $path:expr ),* ], $cont:stmt) => {{
|
||||
$( add_unrecognized!($ty: $path); )*
|
||||
$cont
|
||||
}};
|
||||
}
|
||||
|
||||
// If we fail to list a directory, something bad is happening anyway
|
||||
// (something touches our cache or we have disk failure)
|
||||
// Try to delete it, so we can stay within soft limits of the cache size.
|
||||
// This comment applies later in this function, too.
|
||||
let it = unwrap_or!(
|
||||
fs::read_dir(dir_path),
|
||||
add_unrecognized_and!([dir: dir_path], return),
|
||||
"Failed to list cache directory, deleting it"
|
||||
);
|
||||
|
||||
let mut cache_files = HashMap::new();
|
||||
for entry in it {
|
||||
// read_dir() returns an iterator over results - in case some of them are errors
|
||||
// we don't know their names, so we can't delete them. We don't want to delete
|
||||
// the whole directory with good entries too, so we just ignore the erroneous entries.
|
||||
let entry = unwrap_or!(
|
||||
entry,
|
||||
continue,
|
||||
"Failed to read a cache dir entry (NOT deleting it, it still occupies space)"
|
||||
);
|
||||
let path = entry.path();
|
||||
match (level, path.is_dir()) {
|
||||
(0..=1, true) => enter_dir(vec, &path, level + 1, cache_config),
|
||||
(0..=1, false) => {
|
||||
if level == 0 && path.file_stem() == Some(OsStr::new(".cleanup")) {
|
||||
if path.extension().is_some() {
|
||||
// assume it's cleanup lock
|
||||
if !is_fs_lock_expired(
|
||||
Some(&entry),
|
||||
&path,
|
||||
cache_config.cleanup_interval(),
|
||||
cache_config.allowed_clock_drift_for_files_from_future(),
|
||||
) {
|
||||
continue; // skip active lock
|
||||
}
|
||||
}
|
||||
}
|
||||
add_unrecognized!(file: path);
|
||||
}
|
||||
(2, false) => {
|
||||
let ext = path.extension();
|
||||
if ext.is_none() || ext == Some(OsStr::new("stats")) {
|
||||
// mod or stats file
|
||||
cache_files.insert(path, entry);
|
||||
} else {
|
||||
let recognized = if let Some(ext_str) = ext.unwrap().to_str() {
|
||||
// check if valid lock
|
||||
ext_str.starts_with("wip-")
|
||||
&& !is_fs_lock_expired(
|
||||
Some(&entry),
|
||||
&path,
|
||||
cache_config.optimizing_compression_task_timeout(),
|
||||
cache_config.allowed_clock_drift_for_files_from_future(),
|
||||
)
|
||||
} else {
|
||||
// if it's None, i.e. not valid UTF-8 string, then that's not our lock for sure
|
||||
false
|
||||
};
|
||||
|
||||
if !recognized {
|
||||
add_unrecognized!(file: path);
|
||||
}
|
||||
}
|
||||
}
|
||||
(_, is_dir) => add_unrecognized!(is_dir, path),
|
||||
}
|
||||
}
|
||||
|
||||
// associate module with its stats & handle them
|
||||
// assumption: just mods and stats
|
||||
for (path, entry) in cache_files.iter() {
|
||||
let path_buf: PathBuf;
|
||||
let (mod_, stats_, is_mod) = match path.extension() {
|
||||
Some(_) => {
|
||||
path_buf = path.with_extension("");
|
||||
(
|
||||
cache_files.get(&path_buf).map(|v| (&path_buf, v)),
|
||||
Some((path, entry)),
|
||||
false,
|
||||
)
|
||||
}
|
||||
None => {
|
||||
path_buf = path.with_extension("stats");
|
||||
(
|
||||
Some((path, entry)),
|
||||
cache_files.get(&path_buf).map(|v| (&path_buf, v)),
|
||||
true,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
// construct a cache entry
|
||||
match (mod_, stats_, is_mod) {
|
||||
(Some((mod_path, mod_entry)), Some((stats_path, stats_entry)), true) => {
|
||||
let mod_metadata = unwrap_or!(
|
||||
mod_entry.metadata(),
|
||||
add_unrecognized_and!([file: stats_path, file: mod_path], continue),
|
||||
"Failed to get metadata, deleting BOTH module cache and stats files",
|
||||
mod_path
|
||||
);
|
||||
let stats_mtime = unwrap_or!(
|
||||
stats_entry.metadata().and_then(|m| m.modified()),
|
||||
add_unrecognized_and!(
|
||||
[file: stats_path],
|
||||
unwrap_or!(
|
||||
mod_metadata.modified(),
|
||||
add_unrecognized_and!([file: stats_path, file: mod_path], continue),
|
||||
"Failed to get mtime, deleting BOTH module cache and stats files",
|
||||
mod_path
|
||||
)
|
||||
),
|
||||
"Failed to get metadata/mtime, deleting the file",
|
||||
stats_path
|
||||
);
|
||||
// .into() called for the SystemTimeStub if cfg(test)
|
||||
#[allow(clippy::identity_conversion)]
|
||||
vec.push(CacheEntry::Recognized {
|
||||
path: mod_path.to_path_buf(),
|
||||
mtime: stats_mtime.into(),
|
||||
size: mod_metadata.len(),
|
||||
})
|
||||
}
|
||||
(Some(_), Some(_), false) => (), // was or will be handled by previous branch
|
||||
(Some((mod_path, mod_entry)), None, _) => {
|
||||
let (mod_metadata, mod_mtime) = unwrap_or!(
|
||||
mod_entry
|
||||
.metadata()
|
||||
.and_then(|md| md.modified().map(|mt| (md, mt))),
|
||||
add_unrecognized_and!([file: mod_path], continue),
|
||||
"Failed to get metadata/mtime, deleting the file",
|
||||
mod_path
|
||||
);
|
||||
// .into() called for the SystemTimeStub if cfg(test)
|
||||
#[allow(clippy::identity_conversion)]
|
||||
vec.push(CacheEntry::Recognized {
|
||||
path: mod_path.to_path_buf(),
|
||||
mtime: mod_mtime.into(),
|
||||
size: mod_metadata.len(),
|
||||
})
|
||||
}
|
||||
(None, Some((stats_path, _stats_entry)), _) => {
|
||||
debug!("Found orphaned stats file: {}", stats_path.display());
|
||||
add_unrecognized!(file: stats_path);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut vec = Vec::new();
|
||||
enter_dir(
|
||||
&mut vec,
|
||||
self.cache_config.directory(),
|
||||
0,
|
||||
&self.cache_config,
|
||||
);
|
||||
vec
|
||||
}
|
||||
}
|
||||
|
||||
fn read_stats_file(path: &Path) -> Option<ModuleCacheStatistics> {
|
||||
fs::read(path)
|
||||
.map_err(|err| {
|
||||
trace!(
|
||||
"Failed to read stats file, path: {}, err: {}",
|
||||
path.display(),
|
||||
err
|
||||
)
|
||||
})
|
||||
.and_then(|bytes| {
|
||||
toml::from_slice::<ModuleCacheStatistics>(&bytes[..]).map_err(|err| {
|
||||
trace!(
|
||||
"Failed to parse stats file, path: {}, err: {}",
|
||||
path.display(),
|
||||
err,
|
||||
)
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn write_stats_file(path: &Path, stats: &ModuleCacheStatistics) -> bool {
|
||||
toml::to_string_pretty(&stats)
|
||||
.map_err(|err| {
|
||||
warn!(
|
||||
"Failed to serialize stats file, path: {}, err: {}",
|
||||
path.display(),
|
||||
err
|
||||
)
|
||||
})
|
||||
.and_then(|serialized| {
|
||||
if fs_write_atomic(path, "stats", serialized.as_bytes()) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
})
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Tries to acquire a lock for specific task.
|
||||
///
|
||||
/// Returns Some(path) to the lock if succeeds. The task path must not
|
||||
/// contain any extension and have file stem.
|
||||
///
|
||||
/// To release a lock you need either manually rename or remove it,
|
||||
/// or wait until it expires and cleanup task removes it.
|
||||
///
|
||||
/// Note: this function is racy. Main idea is: be fault tolerant and
|
||||
/// never block some task. The price is that we rarely do some task
|
||||
/// more than once.
|
||||
fn acquire_task_fs_lock(
|
||||
task_path: &Path,
|
||||
timeout: Duration,
|
||||
allowed_future_drift: Duration,
|
||||
) -> Option<PathBuf> {
|
||||
assert!(task_path.extension().is_none());
|
||||
assert!(task_path.file_stem().is_some());
|
||||
|
||||
// list directory
|
||||
let dir_path = task_path.parent()?;
|
||||
let it = fs::read_dir(dir_path)
|
||||
.map_err(|err| {
|
||||
warn!(
|
||||
"Failed to list cache directory, path: {}, err: {}",
|
||||
dir_path.display(),
|
||||
err
|
||||
)
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
// look for existing locks
|
||||
for entry in it {
|
||||
let entry = entry
|
||||
.map_err(|err| {
|
||||
warn!(
|
||||
"Failed to list cache directory, path: {}, err: {}",
|
||||
dir_path.display(),
|
||||
err
|
||||
)
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
let path = entry.path();
|
||||
if path.is_dir() || path.file_stem() != task_path.file_stem() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// check extension and mtime
|
||||
match path.extension() {
|
||||
None => continue,
|
||||
Some(ext) => {
|
||||
if let Some(ext_str) = ext.to_str() {
|
||||
// if it's None, i.e. not valid UTF-8 string, then that's not our lock for sure
|
||||
if ext_str.starts_with("wip-")
|
||||
&& !is_fs_lock_expired(Some(&entry), &path, timeout, allowed_future_drift)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create the lock
|
||||
let lock_path = task_path.with_extension(format!("wip-{}", std::process::id()));
|
||||
let _file = fs::OpenOptions::new()
|
||||
.create_new(true)
|
||||
.write(true)
|
||||
.open(&lock_path)
|
||||
.map_err(|err| {
|
||||
warn!(
|
||||
"Failed to create lock file (note: it shouldn't exists): path: {}, err: {}",
|
||||
lock_path.display(),
|
||||
err
|
||||
)
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
Some(lock_path)
|
||||
}
|
||||
|
||||
// we have either both, or just path; dir entry is desirable since on some platforms we can get
|
||||
// metadata without extra syscalls
|
||||
// futhermore: it's better to get a path if we have it instead of allocating a new one from the dir entry
|
||||
fn is_fs_lock_expired(
|
||||
entry: Option<&fs::DirEntry>,
|
||||
path: &PathBuf,
|
||||
threshold: Duration,
|
||||
allowed_future_drift: Duration,
|
||||
) -> bool {
|
||||
let mtime = match entry
|
||||
.map_or_else(|| path.metadata(), |e| e.metadata())
|
||||
.and_then(|metadata| metadata.modified())
|
||||
{
|
||||
Ok(mt) => mt,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"Failed to get metadata/mtime, treating as an expired lock, path: {}, err: {}",
|
||||
path.display(),
|
||||
err
|
||||
);
|
||||
return true; // can't read mtime, treat as expired, so this task will not be starved
|
||||
}
|
||||
};
|
||||
|
||||
// DON'T use: mtime.elapsed() -- we must call SystemTime directly for the tests to be deterministic
|
||||
match SystemTime::now().duration_since(mtime) {
|
||||
Ok(elapsed) => elapsed >= threshold,
|
||||
Err(err) => {
|
||||
trace!(
|
||||
"Found mtime in the future, treating as a not expired lock, path: {}, err: {}",
|
||||
path.display(),
|
||||
err
|
||||
);
|
||||
// the lock is expired if the time is too far in the future
|
||||
// it is fine to have network share and not synchronized clocks,
|
||||
// but it's not good when user changes time in their system clock
|
||||
err.duration() > allowed_future_drift
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
758
crates/environ/src/cache/worker/tests.rs
vendored
Normal file
758
crates/environ/src/cache/worker/tests.rs
vendored
Normal file
@@ -0,0 +1,758 @@
|
||||
use super::*;
|
||||
use crate::cache::config::tests::test_prolog;
|
||||
use core::iter::repeat;
|
||||
use std::process;
|
||||
// load_config! comes from crate::cache(::config::tests);
|
||||
|
||||
// when doing anything with the tests, make sure they are DETERMINISTIC
|
||||
// -- the result shouldn't rely on system time!
|
||||
pub mod system_time_stub;
|
||||
|
||||
#[test]
|
||||
fn test_on_get_create_stats_file() {
|
||||
let (_tempdir, cache_dir, config_path) = test_prolog();
|
||||
let cache_config = load_config!(
|
||||
config_path,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}",
|
||||
cache_dir
|
||||
);
|
||||
assert!(cache_config.enabled());
|
||||
let worker = Worker::start_new(&cache_config, None);
|
||||
|
||||
let mod_file = cache_dir.join("some-mod");
|
||||
worker.on_cache_get_async(mod_file);
|
||||
worker.wait_for_all_events_handled();
|
||||
assert_eq!(worker.events_dropped(), 0);
|
||||
|
||||
let stats_file = cache_dir.join("some-mod.stats");
|
||||
let stats = read_stats_file(&stats_file).expect("Failed to read stats file");
|
||||
assert_eq!(stats.usages, 1);
|
||||
assert_eq!(
|
||||
stats.compression_level,
|
||||
cache_config.baseline_compression_level()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_on_get_update_usage_counter() {
|
||||
let (_tempdir, cache_dir, config_path) = test_prolog();
|
||||
let cache_config = load_config!(
|
||||
config_path,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
worker-event-queue-size = '16'",
|
||||
cache_dir
|
||||
);
|
||||
assert!(cache_config.enabled());
|
||||
let worker = Worker::start_new(&cache_config, None);
|
||||
|
||||
let mod_file = cache_dir.join("some-mod");
|
||||
let stats_file = cache_dir.join("some-mod.stats");
|
||||
let default_stats = ModuleCacheStatistics::default(&cache_config);
|
||||
assert!(write_stats_file(&stats_file, &default_stats));
|
||||
|
||||
let mut usages = 0;
|
||||
for times_used in &[4, 7, 2] {
|
||||
for _ in 0..*times_used {
|
||||
worker.on_cache_get_async(mod_file.clone());
|
||||
usages += 1;
|
||||
}
|
||||
|
||||
worker.wait_for_all_events_handled();
|
||||
assert_eq!(worker.events_dropped(), 0);
|
||||
|
||||
let stats = read_stats_file(&stats_file).expect("Failed to read stats file");
|
||||
assert_eq!(stats.usages, usages);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_on_get_recompress_no_mod_file() {
|
||||
let (_tempdir, cache_dir, config_path) = test_prolog();
|
||||
let cache_config = load_config!(
|
||||
config_path,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
worker-event-queue-size = '16'\n\
|
||||
baseline-compression-level = 3\n\
|
||||
optimized-compression-level = 7\n\
|
||||
optimized-compression-usage-counter-threshold = '256'",
|
||||
cache_dir
|
||||
);
|
||||
assert!(cache_config.enabled());
|
||||
let worker = Worker::start_new(&cache_config, None);
|
||||
|
||||
let mod_file = cache_dir.join("some-mod");
|
||||
let stats_file = cache_dir.join("some-mod.stats");
|
||||
let mut start_stats = ModuleCacheStatistics::default(&cache_config);
|
||||
start_stats.usages = 250;
|
||||
assert!(write_stats_file(&stats_file, &start_stats));
|
||||
|
||||
let mut usages = start_stats.usages;
|
||||
for times_used in &[4, 7, 2] {
|
||||
for _ in 0..*times_used {
|
||||
worker.on_cache_get_async(mod_file.clone());
|
||||
usages += 1;
|
||||
}
|
||||
|
||||
worker.wait_for_all_events_handled();
|
||||
assert_eq!(worker.events_dropped(), 0);
|
||||
|
||||
let stats = read_stats_file(&stats_file).expect("Failed to read stats file");
|
||||
assert_eq!(stats.usages, usages);
|
||||
assert_eq!(
|
||||
stats.compression_level,
|
||||
cache_config.baseline_compression_level()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_on_get_recompress_with_mod_file() {
|
||||
let (_tempdir, cache_dir, config_path) = test_prolog();
|
||||
let cache_config = load_config!(
|
||||
config_path,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
worker-event-queue-size = '16'\n\
|
||||
baseline-compression-level = 3\n\
|
||||
optimized-compression-level = 7\n\
|
||||
optimized-compression-usage-counter-threshold = '256'",
|
||||
cache_dir
|
||||
);
|
||||
assert!(cache_config.enabled());
|
||||
let worker = Worker::start_new(&cache_config, None);
|
||||
|
||||
let mod_file = cache_dir.join("some-mod");
|
||||
let mod_data = "some test data to be compressed";
|
||||
let data = zstd::encode_all(
|
||||
mod_data.as_bytes(),
|
||||
cache_config.baseline_compression_level(),
|
||||
)
|
||||
.expect("Failed to compress sample mod file");
|
||||
fs::write(&mod_file, &data).expect("Failed to write sample mod file");
|
||||
|
||||
let stats_file = cache_dir.join("some-mod.stats");
|
||||
let mut start_stats = ModuleCacheStatistics::default(&cache_config);
|
||||
start_stats.usages = 250;
|
||||
assert!(write_stats_file(&stats_file, &start_stats));
|
||||
|
||||
// scenarios:
|
||||
// 1. Shouldn't be recompressed
|
||||
// 2. Should be recompressed
|
||||
// 3. After lowering compression level, should be recompressed
|
||||
let scenarios = [(4, false), (7, true), (2, false)];
|
||||
|
||||
let mut usages = start_stats.usages;
|
||||
assert!(usages < cache_config.optimized_compression_usage_counter_threshold());
|
||||
let mut tested_higher_opt_compr_lvl = false;
|
||||
for (times_used, lower_compr_lvl) in &scenarios {
|
||||
for _ in 0..*times_used {
|
||||
worker.on_cache_get_async(mod_file.clone());
|
||||
usages += 1;
|
||||
}
|
||||
|
||||
worker.wait_for_all_events_handled();
|
||||
assert_eq!(worker.events_dropped(), 0);
|
||||
|
||||
let mut stats = read_stats_file(&stats_file).expect("Failed to read stats file");
|
||||
assert_eq!(stats.usages, usages);
|
||||
assert_eq!(
|
||||
stats.compression_level,
|
||||
if usages < cache_config.optimized_compression_usage_counter_threshold() {
|
||||
cache_config.baseline_compression_level()
|
||||
} else {
|
||||
cache_config.optimized_compression_level()
|
||||
}
|
||||
);
|
||||
let compressed_data = fs::read(&mod_file).expect("Failed to read mod file");
|
||||
let decoded_data =
|
||||
zstd::decode_all(&compressed_data[..]).expect("Failed to decompress mod file");
|
||||
assert_eq!(decoded_data, mod_data.as_bytes());
|
||||
|
||||
if *lower_compr_lvl {
|
||||
assert!(usages >= cache_config.optimized_compression_usage_counter_threshold());
|
||||
tested_higher_opt_compr_lvl = true;
|
||||
stats.compression_level -= 1;
|
||||
assert!(write_stats_file(&stats_file, &stats));
|
||||
}
|
||||
}
|
||||
assert!(usages >= cache_config.optimized_compression_usage_counter_threshold());
|
||||
assert!(tested_higher_opt_compr_lvl);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_on_get_recompress_lock() {
|
||||
let (_tempdir, cache_dir, config_path) = test_prolog();
|
||||
let cache_config = load_config!(
|
||||
config_path,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
worker-event-queue-size = '16'\n\
|
||||
baseline-compression-level = 3\n\
|
||||
optimized-compression-level = 7\n\
|
||||
optimized-compression-usage-counter-threshold = '256'\n\
|
||||
optimizing-compression-task-timeout = '30m'\n\
|
||||
allowed-clock-drift-for-files-from-future = '1d'",
|
||||
cache_dir
|
||||
);
|
||||
assert!(cache_config.enabled());
|
||||
let worker = Worker::start_new(&cache_config, None);
|
||||
|
||||
let mod_file = cache_dir.join("some-mod");
|
||||
let mod_data = "some test data to be compressed";
|
||||
let data = zstd::encode_all(
|
||||
mod_data.as_bytes(),
|
||||
cache_config.baseline_compression_level(),
|
||||
)
|
||||
.expect("Failed to compress sample mod file");
|
||||
fs::write(&mod_file, &data).expect("Failed to write sample mod file");
|
||||
|
||||
let stats_file = cache_dir.join("some-mod.stats");
|
||||
let mut start_stats = ModuleCacheStatistics::default(&cache_config);
|
||||
start_stats.usages = 255;
|
||||
|
||||
let lock_file = cache_dir.join("some-mod.wip-lock");
|
||||
|
||||
let scenarios = [
|
||||
// valid lock
|
||||
(true, "past", Duration::from_secs(30 * 60 - 1)),
|
||||
// valid future lock
|
||||
(true, "future", Duration::from_secs(24 * 60 * 60)),
|
||||
// expired lock
|
||||
(false, "past", Duration::from_secs(30 * 60)),
|
||||
// expired future lock
|
||||
(false, "future", Duration::from_secs(24 * 60 * 60 + 1)),
|
||||
];
|
||||
|
||||
for (lock_valid, duration_sign, duration) in &scenarios {
|
||||
assert!(write_stats_file(&stats_file, &start_stats)); // restore usage & compression level
|
||||
create_file_with_mtime(&lock_file, "", duration_sign, &duration);
|
||||
|
||||
worker.on_cache_get_async(mod_file.clone());
|
||||
worker.wait_for_all_events_handled();
|
||||
assert_eq!(worker.events_dropped(), 0);
|
||||
|
||||
let stats = read_stats_file(&stats_file).expect("Failed to read stats file");
|
||||
assert_eq!(stats.usages, start_stats.usages + 1);
|
||||
assert_eq!(
|
||||
stats.compression_level,
|
||||
if *lock_valid {
|
||||
cache_config.baseline_compression_level()
|
||||
} else {
|
||||
cache_config.optimized_compression_level()
|
||||
}
|
||||
);
|
||||
let compressed_data = fs::read(&mod_file).expect("Failed to read mod file");
|
||||
let decoded_data =
|
||||
zstd::decode_all(&compressed_data[..]).expect("Failed to decompress mod file");
|
||||
assert_eq!(decoded_data, mod_data.as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_on_update_fresh_stats_file() {
|
||||
let (_tempdir, cache_dir, config_path) = test_prolog();
|
||||
let cache_config = load_config!(
|
||||
config_path,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
worker-event-queue-size = '16'\n\
|
||||
baseline-compression-level = 3\n\
|
||||
optimized-compression-level = 7\n\
|
||||
cleanup-interval = '1h'",
|
||||
cache_dir
|
||||
);
|
||||
assert!(cache_config.enabled());
|
||||
let worker = Worker::start_new(&cache_config, None);
|
||||
|
||||
let mod_file = cache_dir.join("some-mod");
|
||||
let stats_file = cache_dir.join("some-mod.stats");
|
||||
let cleanup_certificate = cache_dir.join(".cleanup.wip-done");
|
||||
create_file_with_mtime(&cleanup_certificate, "", "future", &Duration::from_secs(0));
|
||||
// the below created by the worker if it cleans up
|
||||
let worker_lock_file = cache_dir.join(format!(".cleanup.wip-{}", process::id()));
|
||||
|
||||
// scenarios:
|
||||
// 1. Create new stats file
|
||||
// 2. Overwrite existing file
|
||||
for update_file in &[true, false] {
|
||||
worker.on_cache_update_async(mod_file.clone());
|
||||
worker.wait_for_all_events_handled();
|
||||
assert_eq!(worker.events_dropped(), 0);
|
||||
|
||||
let mut stats = read_stats_file(&stats_file).expect("Failed to read stats file");
|
||||
assert_eq!(stats.usages, 1);
|
||||
assert_eq!(
|
||||
stats.compression_level,
|
||||
cache_config.baseline_compression_level()
|
||||
);
|
||||
|
||||
if *update_file {
|
||||
stats.usages += 42;
|
||||
stats.compression_level += 1;
|
||||
assert!(write_stats_file(&stats_file, &stats));
|
||||
}
|
||||
|
||||
assert!(!worker_lock_file.exists());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_on_update_cleanup_limits_trash_locks() {
|
||||
let (_tempdir, cache_dir, config_path) = test_prolog();
|
||||
let cache_config = load_config!(
|
||||
config_path,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
worker-event-queue-size = '16'\n\
|
||||
cleanup-interval = '30m'\n\
|
||||
optimizing-compression-task-timeout = '30m'\n\
|
||||
allowed-clock-drift-for-files-from-future = '1d'\n\
|
||||
file-count-soft-limit = '5'\n\
|
||||
files-total-size-soft-limit = '30K'\n\
|
||||
file-count-limit-percent-if-deleting = '70%'\n\
|
||||
files-total-size-limit-percent-if-deleting = '70%'
|
||||
",
|
||||
cache_dir
|
||||
);
|
||||
assert!(cache_config.enabled());
|
||||
let worker = Worker::start_new(&cache_config, None);
|
||||
let content_1k = "a".repeat(1_000);
|
||||
let content_10k = "a".repeat(10_000);
|
||||
|
||||
let mods_files_dir = cache_dir.join("target-triple").join("compiler-version");
|
||||
let mod_with_stats = mods_files_dir.join("mod-with-stats");
|
||||
let trash_dirs = [
|
||||
mods_files_dir.join("trash"),
|
||||
mods_files_dir.join("trash").join("trash"),
|
||||
];
|
||||
let trash_files = [
|
||||
cache_dir.join("trash-file"),
|
||||
cache_dir.join("trash-file.wip-lock"),
|
||||
cache_dir.join("target-triple").join("trash.txt"),
|
||||
cache_dir.join("target-triple").join("trash.txt.wip-lock"),
|
||||
mods_files_dir.join("trash.ogg"),
|
||||
mods_files_dir.join("trash").join("trash.doc"),
|
||||
mods_files_dir.join("trash").join("trash.doc.wip-lock"),
|
||||
mods_files_dir.join("trash").join("trash").join("trash.xls"),
|
||||
mods_files_dir
|
||||
.join("trash")
|
||||
.join("trash")
|
||||
.join("trash.xls.wip-lock"),
|
||||
];
|
||||
let mod_locks = [
|
||||
// valid lock
|
||||
(
|
||||
mods_files_dir.join("mod0.wip-lock"),
|
||||
true,
|
||||
"past",
|
||||
Duration::from_secs(30 * 60 - 1),
|
||||
),
|
||||
// valid future lock
|
||||
(
|
||||
mods_files_dir.join("mod1.wip-lock"),
|
||||
true,
|
||||
"future",
|
||||
Duration::from_secs(24 * 60 * 60),
|
||||
),
|
||||
// expired lock
|
||||
(
|
||||
mods_files_dir.join("mod2.wip-lock"),
|
||||
false,
|
||||
"past",
|
||||
Duration::from_secs(30 * 60),
|
||||
),
|
||||
// expired future lock
|
||||
(
|
||||
mods_files_dir.join("mod3.wip-lock"),
|
||||
false,
|
||||
"future",
|
||||
Duration::from_secs(24 * 60 * 60 + 1),
|
||||
),
|
||||
];
|
||||
// the below created by the worker if it cleans up
|
||||
let worker_lock_file = cache_dir.join(format!(".cleanup.wip-{}", process::id()));
|
||||
|
||||
let scenarios = [
|
||||
// Close to limits, but not reached, only trash deleted
|
||||
(2, 2, 4),
|
||||
// File count limit exceeded
|
||||
(1, 10, 3),
|
||||
// Total size limit exceeded
|
||||
(4, 0, 2),
|
||||
// Both limits exceeded
|
||||
(3, 5, 3),
|
||||
];
|
||||
|
||||
for (files_10k, files_1k, remaining_files) in &scenarios {
|
||||
let mut secs_ago = 100;
|
||||
|
||||
for d in &trash_dirs {
|
||||
fs::create_dir_all(d).expect("Failed to create directories");
|
||||
}
|
||||
for f in &trash_files {
|
||||
create_file_with_mtime(f, "", "past", &Duration::from_secs(0));
|
||||
}
|
||||
for (f, _, sign, duration) in &mod_locks {
|
||||
create_file_with_mtime(f, "", sign, &duration);
|
||||
}
|
||||
|
||||
let mut mods_paths = vec![];
|
||||
for content in repeat(&content_10k)
|
||||
.take(*files_10k)
|
||||
.chain(repeat(&content_1k).take(*files_1k))
|
||||
{
|
||||
mods_paths.push(mods_files_dir.join(format!("test-mod-{}", mods_paths.len())));
|
||||
create_file_with_mtime(
|
||||
mods_paths.last().unwrap(),
|
||||
content,
|
||||
"past",
|
||||
&Duration::from_secs(secs_ago),
|
||||
);
|
||||
assert!(secs_ago > 0);
|
||||
secs_ago -= 1;
|
||||
}
|
||||
|
||||
// creating .stats file updates mtime what affects test results
|
||||
// so we use a separate nonexistent module here (orphaned .stats will be removed anyway)
|
||||
worker.on_cache_update_async(mod_with_stats.clone());
|
||||
worker.wait_for_all_events_handled();
|
||||
assert_eq!(worker.events_dropped(), 0);
|
||||
|
||||
for ent in trash_dirs.iter().chain(trash_files.iter()) {
|
||||
assert!(!ent.exists());
|
||||
}
|
||||
for (f, valid, ..) in &mod_locks {
|
||||
assert_eq!(f.exists(), *valid);
|
||||
}
|
||||
for (idx, path) in mods_paths.iter().enumerate() {
|
||||
let should_exist = idx >= mods_paths.len() - *remaining_files;
|
||||
assert_eq!(path.exists(), should_exist);
|
||||
if should_exist {
|
||||
// cleanup before next iteration
|
||||
fs::remove_file(path).expect("Failed to remove a file");
|
||||
}
|
||||
}
|
||||
fs::remove_file(&worker_lock_file).expect("Failed to remove lock file");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_on_update_cleanup_lru_policy() {
|
||||
let (_tempdir, cache_dir, config_path) = test_prolog();
|
||||
let cache_config = load_config!(
|
||||
config_path,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
worker-event-queue-size = '16'\n\
|
||||
file-count-soft-limit = '5'\n\
|
||||
files-total-size-soft-limit = '30K'\n\
|
||||
file-count-limit-percent-if-deleting = '80%'\n\
|
||||
files-total-size-limit-percent-if-deleting = '70%'",
|
||||
cache_dir
|
||||
);
|
||||
assert!(cache_config.enabled());
|
||||
let worker = Worker::start_new(&cache_config, None);
|
||||
let content_1k = "a".repeat(1_000);
|
||||
let content_5k = "a".repeat(5_000);
|
||||
let content_10k = "a".repeat(10_000);
|
||||
|
||||
let mods_files_dir = cache_dir.join("target-triple").join("compiler-version");
|
||||
fs::create_dir_all(&mods_files_dir).expect("Failed to create directories");
|
||||
let nonexistent_mod_file = cache_dir.join("nonexistent-mod");
|
||||
let orphaned_stats_file = cache_dir.join("orphaned-mod.stats");
|
||||
let worker_lock_file = cache_dir.join(format!(".cleanup.wip-{}", process::id()));
|
||||
|
||||
// content, how long ago created, how long ago stats created (if created), should be alive
|
||||
let scenarios = [
|
||||
&[
|
||||
(&content_10k, 29, None, false),
|
||||
(&content_10k, 28, None, false),
|
||||
(&content_10k, 27, None, false),
|
||||
(&content_1k, 26, None, true),
|
||||
(&content_10k, 25, None, true),
|
||||
(&content_1k, 24, None, true),
|
||||
],
|
||||
&[
|
||||
(&content_10k, 29, None, false),
|
||||
(&content_10k, 28, None, false),
|
||||
(&content_10k, 27, None, true),
|
||||
(&content_1k, 26, None, true),
|
||||
(&content_5k, 25, None, true),
|
||||
(&content_1k, 24, None, true),
|
||||
],
|
||||
&[
|
||||
(&content_10k, 29, Some(19), true),
|
||||
(&content_10k, 28, None, false),
|
||||
(&content_10k, 27, None, false),
|
||||
(&content_1k, 26, Some(18), true),
|
||||
(&content_5k, 25, None, true),
|
||||
(&content_1k, 24, None, true),
|
||||
],
|
||||
&[
|
||||
(&content_10k, 29, Some(19), true),
|
||||
(&content_10k, 28, Some(18), true),
|
||||
(&content_10k, 27, None, false),
|
||||
(&content_1k, 26, Some(17), true),
|
||||
(&content_5k, 25, None, false),
|
||||
(&content_1k, 24, None, false),
|
||||
],
|
||||
&[
|
||||
(&content_10k, 29, Some(19), true),
|
||||
(&content_10k, 28, None, false),
|
||||
(&content_1k, 27, None, false),
|
||||
(&content_5k, 26, Some(18), true),
|
||||
(&content_1k, 25, None, false),
|
||||
(&content_10k, 24, None, false),
|
||||
],
|
||||
];
|
||||
|
||||
for mods in &scenarios {
|
||||
let filenames = (0..mods.len())
|
||||
.map(|i| {
|
||||
(
|
||||
mods_files_dir.join(format!("mod-{}", i)),
|
||||
mods_files_dir.join(format!("mod-{}.stats", i)),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for ((content, mod_secs_ago, create_stats, _), (mod_filename, stats_filename)) in
|
||||
mods.iter().zip(filenames.iter())
|
||||
{
|
||||
create_file_with_mtime(
|
||||
mod_filename,
|
||||
content,
|
||||
"past",
|
||||
&Duration::from_secs(*mod_secs_ago),
|
||||
);
|
||||
if let Some(stats_secs_ago) = create_stats {
|
||||
create_file_with_mtime(
|
||||
stats_filename,
|
||||
"cleanup doesn't care",
|
||||
"past",
|
||||
&Duration::from_secs(*stats_secs_ago),
|
||||
);
|
||||
}
|
||||
}
|
||||
create_file_with_mtime(
|
||||
&orphaned_stats_file,
|
||||
"cleanup doesn't care",
|
||||
"past",
|
||||
&Duration::from_secs(0),
|
||||
);
|
||||
|
||||
worker.on_cache_update_async(nonexistent_mod_file.clone());
|
||||
worker.wait_for_all_events_handled();
|
||||
assert_eq!(worker.events_dropped(), 0);
|
||||
|
||||
assert!(!orphaned_stats_file.exists());
|
||||
for ((_, _, create_stats, alive), (mod_filename, stats_filename)) in
|
||||
mods.iter().zip(filenames.iter())
|
||||
{
|
||||
assert_eq!(mod_filename.exists(), *alive);
|
||||
assert_eq!(stats_filename.exists(), *alive && create_stats.is_some());
|
||||
|
||||
// cleanup for next iteration
|
||||
if *alive {
|
||||
fs::remove_file(&mod_filename).expect("Failed to remove a file");
|
||||
if create_stats.is_some() {
|
||||
fs::remove_file(&stats_filename).expect("Failed to remove a file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs::remove_file(&worker_lock_file).expect("Failed to remove lock file");
|
||||
}
|
||||
}
|
||||
|
||||
// clock drift should be applied to mod cache & stats, too
|
||||
// however, postpone deleting files to as late as possible
|
||||
#[test]
|
||||
fn test_on_update_cleanup_future_files() {
|
||||
let (_tempdir, cache_dir, config_path) = test_prolog();
|
||||
let cache_config = load_config!(
|
||||
config_path,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
worker-event-queue-size = '16'\n\
|
||||
allowed-clock-drift-for-files-from-future = '1d'\n\
|
||||
file-count-soft-limit = '3'\n\
|
||||
files-total-size-soft-limit = '1M'\n\
|
||||
file-count-limit-percent-if-deleting = '70%'\n\
|
||||
files-total-size-limit-percent-if-deleting = '70%'",
|
||||
cache_dir
|
||||
);
|
||||
assert!(cache_config.enabled());
|
||||
let worker = Worker::start_new(&cache_config, None);
|
||||
let content_1k = "a".repeat(1_000);
|
||||
|
||||
let mods_files_dir = cache_dir.join("target-triple").join("compiler-version");
|
||||
fs::create_dir_all(&mods_files_dir).expect("Failed to create directories");
|
||||
let nonexistent_mod_file = cache_dir.join("nonexistent-mod");
|
||||
// the below created by the worker if it cleans up
|
||||
let worker_lock_file = cache_dir.join(format!(".cleanup.wip-{}", process::id()));
|
||||
|
||||
let scenarios: [&[_]; 5] = [
|
||||
// NOT cleaning up, everythings ok
|
||||
&[
|
||||
(Duration::from_secs(0), None, true),
|
||||
(Duration::from_secs(24 * 60 * 60), None, true),
|
||||
],
|
||||
// NOT cleaning up, everythings ok
|
||||
&[
|
||||
(Duration::from_secs(0), None, true),
|
||||
(Duration::from_secs(24 * 60 * 60 + 1), None, true),
|
||||
],
|
||||
// cleaning up, removing files from oldest
|
||||
&[
|
||||
(Duration::from_secs(0), None, false),
|
||||
(Duration::from_secs(24 * 60 * 60), None, true),
|
||||
(Duration::from_secs(1), None, false),
|
||||
(Duration::from_secs(2), None, true),
|
||||
],
|
||||
// cleaning up, removing files from oldest; deleting file from far future
|
||||
&[
|
||||
(Duration::from_secs(0), None, false),
|
||||
(Duration::from_secs(1), None, true),
|
||||
(Duration::from_secs(24 * 60 * 60 + 1), None, false),
|
||||
(Duration::from_secs(2), None, true),
|
||||
],
|
||||
// cleaning up, removing files from oldest; file from far future should have .stats from +-now => it's a legitimate file
|
||||
&[
|
||||
(Duration::from_secs(0), None, false),
|
||||
(Duration::from_secs(1), None, false),
|
||||
(
|
||||
Duration::from_secs(24 * 60 * 60 + 1),
|
||||
Some(Duration::from_secs(3)),
|
||||
true,
|
||||
),
|
||||
(Duration::from_secs(2), None, true),
|
||||
],
|
||||
];
|
||||
|
||||
for mods in &scenarios {
|
||||
let filenames = (0..mods.len())
|
||||
.map(|i| {
|
||||
(
|
||||
mods_files_dir.join(format!("mod-{}", i)),
|
||||
mods_files_dir.join(format!("mod-{}.stats", i)),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for ((duration, opt_stats_duration, _), (mod_filename, stats_filename)) in
|
||||
mods.iter().zip(filenames.iter())
|
||||
{
|
||||
create_file_with_mtime(mod_filename, &content_1k, "future", duration);
|
||||
if let Some(stats_duration) = opt_stats_duration {
|
||||
create_file_with_mtime(stats_filename, "", "future", stats_duration);
|
||||
}
|
||||
}
|
||||
|
||||
worker.on_cache_update_async(nonexistent_mod_file.clone());
|
||||
worker.wait_for_all_events_handled();
|
||||
assert_eq!(worker.events_dropped(), 0);
|
||||
|
||||
for ((_, opt_stats_duration, alive), (mod_filename, stats_filename)) in
|
||||
mods.iter().zip(filenames.iter())
|
||||
{
|
||||
assert_eq!(mod_filename.exists(), *alive);
|
||||
assert_eq!(
|
||||
stats_filename.exists(),
|
||||
*alive && opt_stats_duration.is_some()
|
||||
);
|
||||
if *alive {
|
||||
fs::remove_file(mod_filename).expect("Failed to remove a file");
|
||||
if opt_stats_duration.is_some() {
|
||||
fs::remove_file(stats_filename).expect("Failed to remove a file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs::remove_file(&worker_lock_file).expect("Failed to remove lock file");
|
||||
}
|
||||
}
|
||||
|
||||
// this tests if worker triggered cleanup or not when some cleanup lock/certificate was out there
|
||||
#[test]
|
||||
fn test_on_update_cleanup_self_lock() {
|
||||
let (_tempdir, cache_dir, config_path) = test_prolog();
|
||||
let cache_config = load_config!(
|
||||
config_path,
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {cache_dir}\n\
|
||||
worker-event-queue-size = '16'\n\
|
||||
cleanup-interval = '30m'\n\
|
||||
allowed-clock-drift-for-files-from-future = '1d'",
|
||||
cache_dir
|
||||
);
|
||||
assert!(cache_config.enabled());
|
||||
let worker = Worker::start_new(&cache_config, None);
|
||||
|
||||
let mod_file = cache_dir.join("some-mod");
|
||||
let trash_file = cache_dir.join("trash-file.txt");
|
||||
|
||||
let lock_file = cache_dir.join(".cleanup.wip-lock");
|
||||
// the below created by the worker if it cleans up
|
||||
let worker_lock_file = cache_dir.join(format!(".cleanup.wip-{}", process::id()));
|
||||
|
||||
let scenarios = [
|
||||
// valid lock
|
||||
(true, "past", Duration::from_secs(30 * 60 - 1)),
|
||||
// valid future lock
|
||||
(true, "future", Duration::from_secs(24 * 60 * 60)),
|
||||
// expired lock
|
||||
(false, "past", Duration::from_secs(30 * 60)),
|
||||
// expired future lock
|
||||
(false, "future", Duration::from_secs(24 * 60 * 60 + 1)),
|
||||
];
|
||||
|
||||
for (lock_valid, duration_sign, duration) in &scenarios {
|
||||
create_file_with_mtime(
|
||||
&trash_file,
|
||||
"with trash content",
|
||||
"future",
|
||||
&Duration::from_secs(0),
|
||||
);
|
||||
create_file_with_mtime(&lock_file, "", duration_sign, &duration);
|
||||
|
||||
worker.on_cache_update_async(mod_file.clone());
|
||||
worker.wait_for_all_events_handled();
|
||||
assert_eq!(worker.events_dropped(), 0);
|
||||
|
||||
assert_eq!(trash_file.exists(), *lock_valid);
|
||||
assert_eq!(lock_file.exists(), *lock_valid);
|
||||
if *lock_valid {
|
||||
assert!(!worker_lock_file.exists());
|
||||
} else {
|
||||
fs::remove_file(&worker_lock_file).expect("Failed to remove lock file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_file_with_mtime(filename: &Path, contents: &str, offset_sign: &str, offset: &Duration) {
|
||||
fs::write(filename, contents).expect("Failed to create a file");
|
||||
let mtime = match offset_sign {
|
||||
"past" => system_time_stub::NOW
|
||||
.checked_sub(*offset)
|
||||
.expect("Failed to calculate new mtime"),
|
||||
"future" => system_time_stub::NOW
|
||||
.checked_add(*offset)
|
||||
.expect("Failed to calculate new mtime"),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
filetime::set_file_mtime(filename, mtime.into()).expect("Failed to set mtime");
|
||||
}
|
||||
29
crates/environ/src/cache/worker/tests/system_time_stub.rs
vendored
Normal file
29
crates/environ/src/cache/worker/tests/system_time_stub.rs
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
use lazy_static::lazy_static;
|
||||
use std::time::{Duration, SystemTime, SystemTimeError};
|
||||
|
||||
lazy_static! {
|
||||
pub static ref NOW: SystemTime = SystemTime::now(); // no need for RefCell and set_now() for now
|
||||
}
|
||||
|
||||
#[derive(PartialOrd, PartialEq, Ord, Eq)]
|
||||
pub struct SystemTimeStub(SystemTime);
|
||||
|
||||
impl SystemTimeStub {
|
||||
pub fn now() -> Self {
|
||||
Self(*NOW)
|
||||
}
|
||||
|
||||
pub fn checked_add(&self, duration: Duration) -> Option<Self> {
|
||||
self.0.checked_add(duration).map(|t| t.into())
|
||||
}
|
||||
|
||||
pub fn duration_since(&self, earlier: SystemTime) -> Result<Duration, SystemTimeError> {
|
||||
self.0.duration_since(earlier)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SystemTime> for SystemTimeStub {
|
||||
fn from(time: SystemTime) -> Self {
|
||||
Self(time)
|
||||
}
|
||||
}
|
||||
187
crates/environ/src/compilation.rs
Normal file
187
crates/environ/src/compilation.rs
Normal file
@@ -0,0 +1,187 @@
|
||||
//! A `Compilation` contains the compiled function bodies for a WebAssembly
|
||||
//! module.
|
||||
|
||||
use crate::address_map::{ModuleAddressMap, ValueLabelsRanges};
|
||||
use crate::module;
|
||||
use crate::module_environ::FunctionBodyData;
|
||||
use alloc::vec::Vec;
|
||||
use cranelift_codegen::{binemit, ir, isa, CodegenError};
|
||||
use cranelift_entity::PrimaryMap;
|
||||
use cranelift_wasm::{DefinedFuncIndex, FuncIndex, ModuleTranslationState, WasmError};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::Range;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Compiled function: machine code body, jump table offsets, and unwind information.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CompiledFunction {
|
||||
/// The function body.
|
||||
pub body: Vec<u8>,
|
||||
|
||||
/// The jump tables offsets (in the body).
|
||||
pub jt_offsets: ir::JumpTableOffsets,
|
||||
|
||||
/// The unwind information.
|
||||
pub unwind_info: Vec<u8>,
|
||||
}
|
||||
|
||||
type Functions = PrimaryMap<DefinedFuncIndex, CompiledFunction>;
|
||||
|
||||
/// The result of compiling a WebAssembly module's functions.
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq)]
|
||||
pub struct Compilation {
|
||||
/// Compiled machine code for the function bodies.
|
||||
functions: Functions,
|
||||
}
|
||||
|
||||
impl Compilation {
|
||||
/// Creates a compilation artifact from a contiguous function buffer and a set of ranges
|
||||
pub fn new(functions: Functions) -> Self {
|
||||
Self { functions }
|
||||
}
|
||||
|
||||
/// Allocates the compilation result with the given function bodies.
|
||||
pub fn from_buffer(
|
||||
buffer: Vec<u8>,
|
||||
functions: impl IntoIterator<Item = (Range<usize>, ir::JumpTableOffsets, Range<usize>)>,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
functions
|
||||
.into_iter()
|
||||
.map(|(body_range, jt_offsets, unwind_range)| CompiledFunction {
|
||||
body: buffer[body_range].to_vec(),
|
||||
jt_offsets,
|
||||
unwind_info: buffer[unwind_range].to_vec(),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Gets the bytes of a single function
|
||||
pub fn get(&self, func: DefinedFuncIndex) -> &CompiledFunction {
|
||||
&self.functions[func]
|
||||
}
|
||||
|
||||
/// Gets the number of functions defined.
|
||||
pub fn len(&self) -> usize {
|
||||
self.functions.len()
|
||||
}
|
||||
|
||||
/// Gets functions jump table offsets.
|
||||
pub fn get_jt_offsets(&self) -> PrimaryMap<DefinedFuncIndex, ir::JumpTableOffsets> {
|
||||
self.functions
|
||||
.iter()
|
||||
.map(|(_, func)| func.jt_offsets.clone())
|
||||
.collect::<PrimaryMap<DefinedFuncIndex, _>>()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a Compilation {
|
||||
type IntoIter = Iter<'a>;
|
||||
type Item = <Self::IntoIter as Iterator>::Item;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
Iter {
|
||||
iterator: self.functions.iter(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Iter<'a> {
|
||||
iterator: <&'a Functions as IntoIterator>::IntoIter,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Iter<'a> {
|
||||
type Item = &'a CompiledFunction;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.iterator.next().map(|(_, b)| b)
|
||||
}
|
||||
}
|
||||
|
||||
/// A record of a relocation to perform.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Relocation {
|
||||
/// The relocation code.
|
||||
pub reloc: binemit::Reloc,
|
||||
/// Relocation target.
|
||||
pub reloc_target: RelocationTarget,
|
||||
/// The offset where to apply the relocation.
|
||||
pub offset: binemit::CodeOffset,
|
||||
/// The addend to add to the relocation value.
|
||||
pub addend: binemit::Addend,
|
||||
}
|
||||
|
||||
/// Destination function. Can be either user function or some special one, like `memory.grow`.
|
||||
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum RelocationTarget {
|
||||
/// The user function index.
|
||||
UserFunc(FuncIndex),
|
||||
/// A compiler-generated libcall.
|
||||
LibCall(ir::LibCall),
|
||||
/// Function for growing a locally-defined 32-bit memory by the specified amount of pages.
|
||||
Memory32Grow,
|
||||
/// Function for growing an imported 32-bit memory by the specified amount of pages.
|
||||
ImportedMemory32Grow,
|
||||
/// Function for query current size of a locally-defined 32-bit linear memory.
|
||||
Memory32Size,
|
||||
/// Function for query current size of an imported 32-bit linear memory.
|
||||
ImportedMemory32Size,
|
||||
/// Jump table index.
|
||||
JumpTable(FuncIndex, ir::JumpTable),
|
||||
}
|
||||
|
||||
/// Relocations to apply to function bodies.
|
||||
pub type Relocations = PrimaryMap<DefinedFuncIndex, Vec<Relocation>>;
|
||||
|
||||
/// Information about trap.
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
pub struct TrapInformation {
|
||||
/// The offset of the trapping instruction in native code. It is relative to the beginning of the function.
|
||||
pub code_offset: binemit::CodeOffset,
|
||||
/// Location of trapping instruction in WebAssembly binary module.
|
||||
pub source_loc: ir::SourceLoc,
|
||||
/// Code of the trap.
|
||||
pub trap_code: ir::TrapCode,
|
||||
}
|
||||
|
||||
/// Information about traps associated with the functions where the traps are placed.
|
||||
pub type Traps = PrimaryMap<DefinedFuncIndex, Vec<TrapInformation>>;
|
||||
|
||||
/// An error while compiling WebAssembly to machine code.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum CompileError {
|
||||
/// A wasm translation error occured.
|
||||
#[error("WebAssembly translation error: {0}")]
|
||||
Wasm(#[from] WasmError),
|
||||
|
||||
/// A compilation error occured.
|
||||
#[error("Compilation error: {0}")]
|
||||
Codegen(#[from] CodegenError),
|
||||
|
||||
/// A compilation error occured.
|
||||
#[error("Debug info is not supported with this configuration")]
|
||||
DebugInfoNotSupported,
|
||||
}
|
||||
|
||||
/// An implementation of a compiler from parsed WebAssembly module to native code.
|
||||
pub trait Compiler {
|
||||
/// Compile a parsed module with the given `TargetIsa`.
|
||||
fn compile_module<'data, 'module>(
|
||||
module: &'module module::Module,
|
||||
module_translation: &ModuleTranslationState,
|
||||
function_body_inputs: PrimaryMap<DefinedFuncIndex, FunctionBodyData<'data>>,
|
||||
isa: &dyn isa::TargetIsa,
|
||||
generate_debug_info: bool,
|
||||
) -> Result<
|
||||
(
|
||||
Compilation,
|
||||
Relocations,
|
||||
ModuleAddressMap,
|
||||
ValueLabelsRanges,
|
||||
PrimaryMap<DefinedFuncIndex, ir::StackSlots>,
|
||||
Traps,
|
||||
),
|
||||
CompileError,
|
||||
>;
|
||||
}
|
||||
322
crates/environ/src/cranelift.rs
Normal file
322
crates/environ/src/cranelift.rs
Normal file
@@ -0,0 +1,322 @@
|
||||
//! Support for compiling with Cranelift.
|
||||
|
||||
use crate::address_map::{
|
||||
FunctionAddressMap, InstructionAddressMap, ModuleAddressMap, ValueLabelsRanges,
|
||||
};
|
||||
use crate::cache::{ModuleCacheData, ModuleCacheEntry};
|
||||
use crate::compilation::{
|
||||
Compilation, CompileError, CompiledFunction, Relocation, RelocationTarget, Relocations,
|
||||
TrapInformation, Traps,
|
||||
};
|
||||
use crate::func_environ::{
|
||||
get_func_name, get_imported_memory32_grow_name, get_imported_memory32_size_name,
|
||||
get_memory32_grow_name, get_memory32_size_name, FuncEnvironment,
|
||||
};
|
||||
use crate::module::Module;
|
||||
use crate::module_environ::FunctionBodyData;
|
||||
use alloc::vec::Vec;
|
||||
use cranelift_codegen::binemit;
|
||||
use cranelift_codegen::ir;
|
||||
use cranelift_codegen::ir::ExternalName;
|
||||
use cranelift_codegen::isa;
|
||||
use cranelift_codegen::Context;
|
||||
use cranelift_entity::PrimaryMap;
|
||||
use cranelift_wasm::{DefinedFuncIndex, FuncIndex, FuncTranslator, ModuleTranslationState};
|
||||
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
|
||||
|
||||
/// Implementation of a relocation sink that just saves all the information for later
|
||||
pub struct RelocSink {
|
||||
/// Current function index.
|
||||
func_index: FuncIndex,
|
||||
|
||||
/// Relocations recorded for the function.
|
||||
pub func_relocs: Vec<Relocation>,
|
||||
}
|
||||
|
||||
impl binemit::RelocSink for RelocSink {
|
||||
fn reloc_ebb(
|
||||
&mut self,
|
||||
_offset: binemit::CodeOffset,
|
||||
_reloc: binemit::Reloc,
|
||||
_ebb_offset: binemit::CodeOffset,
|
||||
) {
|
||||
// This should use the `offsets` field of `ir::Function`.
|
||||
panic!("ebb headers not yet implemented");
|
||||
}
|
||||
fn reloc_external(
|
||||
&mut self,
|
||||
offset: binemit::CodeOffset,
|
||||
reloc: binemit::Reloc,
|
||||
name: &ExternalName,
|
||||
addend: binemit::Addend,
|
||||
) {
|
||||
let reloc_target = if *name == get_memory32_grow_name() {
|
||||
RelocationTarget::Memory32Grow
|
||||
} else if *name == get_imported_memory32_grow_name() {
|
||||
RelocationTarget::ImportedMemory32Grow
|
||||
} else if *name == get_memory32_size_name() {
|
||||
RelocationTarget::Memory32Size
|
||||
} else if *name == get_imported_memory32_size_name() {
|
||||
RelocationTarget::ImportedMemory32Size
|
||||
} else if let ExternalName::User { namespace, index } = *name {
|
||||
debug_assert!(namespace == 0);
|
||||
RelocationTarget::UserFunc(FuncIndex::from_u32(index))
|
||||
} else if let ExternalName::LibCall(libcall) = *name {
|
||||
RelocationTarget::LibCall(libcall)
|
||||
} else {
|
||||
panic!("unrecognized external name")
|
||||
};
|
||||
self.func_relocs.push(Relocation {
|
||||
reloc,
|
||||
reloc_target,
|
||||
offset,
|
||||
addend,
|
||||
});
|
||||
}
|
||||
|
||||
fn reloc_constant(
|
||||
&mut self,
|
||||
_code_offset: binemit::CodeOffset,
|
||||
_reloc: binemit::Reloc,
|
||||
_constant_offset: ir::ConstantOffset,
|
||||
) {
|
||||
// Do nothing for now: cranelift emits constant data after the function code and also emits
|
||||
// function code with correct relative offsets to the constant data.
|
||||
}
|
||||
|
||||
fn reloc_jt(&mut self, offset: binemit::CodeOffset, reloc: binemit::Reloc, jt: ir::JumpTable) {
|
||||
self.func_relocs.push(Relocation {
|
||||
reloc,
|
||||
reloc_target: RelocationTarget::JumpTable(self.func_index, jt),
|
||||
offset,
|
||||
addend: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl RelocSink {
|
||||
/// Return a new `RelocSink` instance.
|
||||
pub fn new(func_index: FuncIndex) -> Self {
|
||||
Self {
|
||||
func_index,
|
||||
func_relocs: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TrapSink {
|
||||
pub traps: Vec<TrapInformation>,
|
||||
}
|
||||
|
||||
impl TrapSink {
|
||||
fn new() -> Self {
|
||||
Self { traps: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
impl binemit::TrapSink for TrapSink {
|
||||
fn trap(
|
||||
&mut self,
|
||||
code_offset: binemit::CodeOffset,
|
||||
source_loc: ir::SourceLoc,
|
||||
trap_code: ir::TrapCode,
|
||||
) {
|
||||
self.traps.push(TrapInformation {
|
||||
code_offset,
|
||||
source_loc,
|
||||
trap_code,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn get_function_address_map<'data>(
|
||||
context: &Context,
|
||||
data: &FunctionBodyData<'data>,
|
||||
body_len: usize,
|
||||
isa: &dyn isa::TargetIsa,
|
||||
) -> FunctionAddressMap {
|
||||
let mut instructions = Vec::new();
|
||||
|
||||
let func = &context.func;
|
||||
let mut ebbs = func.layout.ebbs().collect::<Vec<_>>();
|
||||
ebbs.sort_by_key(|ebb| func.offsets[*ebb]); // Ensure inst offsets always increase
|
||||
|
||||
let encinfo = isa.encoding_info();
|
||||
for ebb in ebbs {
|
||||
for (offset, inst, size) in func.inst_offsets(ebb, &encinfo) {
|
||||
let srcloc = func.srclocs[inst];
|
||||
instructions.push(InstructionAddressMap {
|
||||
srcloc,
|
||||
code_offset: offset as usize,
|
||||
code_len: size as usize,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Generate artificial srcloc for function start/end to identify boundary
|
||||
// within module. Similar to FuncTranslator::cur_srcloc(): it will wrap around
|
||||
// if byte code is larger than 4 GB.
|
||||
let start_srcloc = ir::SourceLoc::new(data.module_offset as u32);
|
||||
let end_srcloc = ir::SourceLoc::new((data.module_offset + data.data.len()) as u32);
|
||||
|
||||
FunctionAddressMap {
|
||||
instructions,
|
||||
start_srcloc,
|
||||
end_srcloc,
|
||||
body_offset: 0,
|
||||
body_len,
|
||||
}
|
||||
}
|
||||
|
||||
/// A compiler that compiles a WebAssembly module with Cranelift, translating the Wasm to Cranelift IR,
|
||||
/// optimizing it and then translating to assembly.
|
||||
pub struct Cranelift;
|
||||
|
||||
impl crate::compilation::Compiler for Cranelift {
|
||||
/// Compile the module using Cranelift, producing a compilation result with
|
||||
/// associated relocations.
|
||||
fn compile_module<'data, 'module>(
|
||||
module: &'module Module,
|
||||
module_translation: &ModuleTranslationState,
|
||||
function_body_inputs: PrimaryMap<DefinedFuncIndex, FunctionBodyData<'data>>,
|
||||
isa: &dyn isa::TargetIsa,
|
||||
generate_debug_info: bool,
|
||||
) -> Result<
|
||||
(
|
||||
Compilation,
|
||||
Relocations,
|
||||
ModuleAddressMap,
|
||||
ValueLabelsRanges,
|
||||
PrimaryMap<DefinedFuncIndex, ir::StackSlots>,
|
||||
Traps,
|
||||
),
|
||||
CompileError,
|
||||
> {
|
||||
let cache_entry = ModuleCacheEntry::new(
|
||||
module,
|
||||
&function_body_inputs,
|
||||
isa,
|
||||
"cranelift",
|
||||
generate_debug_info,
|
||||
);
|
||||
|
||||
let data = match cache_entry.get_data() {
|
||||
Some(data) => data,
|
||||
None => {
|
||||
let mut functions = PrimaryMap::with_capacity(function_body_inputs.len());
|
||||
let mut relocations = PrimaryMap::with_capacity(function_body_inputs.len());
|
||||
let mut address_transforms = PrimaryMap::with_capacity(function_body_inputs.len());
|
||||
let mut value_ranges = PrimaryMap::with_capacity(function_body_inputs.len());
|
||||
let mut stack_slots = PrimaryMap::with_capacity(function_body_inputs.len());
|
||||
let mut traps = PrimaryMap::with_capacity(function_body_inputs.len());
|
||||
|
||||
function_body_inputs
|
||||
.into_iter()
|
||||
.collect::<Vec<(DefinedFuncIndex, &FunctionBodyData<'data>)>>()
|
||||
.par_iter()
|
||||
.map_init(
|
||||
|| FuncTranslator::new(),
|
||||
|func_translator, (i, input)| {
|
||||
let func_index = module.func_index(*i);
|
||||
let mut context = Context::new();
|
||||
context.func.name = get_func_name(func_index);
|
||||
context.func.signature =
|
||||
module.signatures[module.functions[func_index]].clone();
|
||||
if generate_debug_info {
|
||||
context.func.collect_debug_info();
|
||||
}
|
||||
|
||||
func_translator.translate(
|
||||
module_translation,
|
||||
input.data,
|
||||
input.module_offset,
|
||||
&mut context.func,
|
||||
&mut FuncEnvironment::new(isa.frontend_config(), module),
|
||||
)?;
|
||||
|
||||
let mut code_buf: Vec<u8> = Vec::new();
|
||||
let mut unwind_info = Vec::new();
|
||||
let mut reloc_sink = RelocSink::new(func_index);
|
||||
let mut trap_sink = TrapSink::new();
|
||||
let mut stackmap_sink = binemit::NullStackmapSink {};
|
||||
context.compile_and_emit(
|
||||
isa,
|
||||
&mut code_buf,
|
||||
&mut reloc_sink,
|
||||
&mut trap_sink,
|
||||
&mut stackmap_sink,
|
||||
)?;
|
||||
|
||||
context.emit_unwind_info(isa, &mut unwind_info);
|
||||
|
||||
let address_transform = if generate_debug_info {
|
||||
let body_len = code_buf.len();
|
||||
Some(get_function_address_map(&context, input, body_len, isa))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let ranges = if generate_debug_info {
|
||||
Some(context.build_value_labels_ranges(isa)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok((
|
||||
code_buf,
|
||||
context.func.jt_offsets,
|
||||
reloc_sink.func_relocs,
|
||||
address_transform,
|
||||
ranges,
|
||||
context.func.stack_slots,
|
||||
trap_sink.traps,
|
||||
unwind_info,
|
||||
))
|
||||
},
|
||||
)
|
||||
.collect::<Result<Vec<_>, CompileError>>()?
|
||||
.into_iter()
|
||||
.for_each(
|
||||
|(
|
||||
function,
|
||||
func_jt_offsets,
|
||||
relocs,
|
||||
address_transform,
|
||||
ranges,
|
||||
sss,
|
||||
function_traps,
|
||||
unwind_info,
|
||||
)| {
|
||||
functions.push(CompiledFunction {
|
||||
body: function,
|
||||
jt_offsets: func_jt_offsets,
|
||||
unwind_info,
|
||||
});
|
||||
relocations.push(relocs);
|
||||
if let Some(address_transform) = address_transform {
|
||||
address_transforms.push(address_transform);
|
||||
}
|
||||
value_ranges.push(ranges.unwrap_or_default());
|
||||
stack_slots.push(sss);
|
||||
traps.push(function_traps);
|
||||
},
|
||||
);
|
||||
|
||||
// TODO: Reorganize where we create the Vec for the resolved imports.
|
||||
|
||||
let data = ModuleCacheData::from_tuple((
|
||||
Compilation::new(functions),
|
||||
relocations,
|
||||
address_transforms,
|
||||
value_ranges,
|
||||
stack_slots,
|
||||
traps,
|
||||
));
|
||||
cache_entry.update_data(&data);
|
||||
data
|
||||
}
|
||||
};
|
||||
|
||||
Ok(data.to_tuple())
|
||||
}
|
||||
}
|
||||
707
crates/environ/src/func_environ.rs
Normal file
707
crates/environ/src/func_environ.rs
Normal file
@@ -0,0 +1,707 @@
|
||||
use crate::module::{MemoryPlan, MemoryStyle, Module, TableStyle};
|
||||
use crate::vmoffsets::VMOffsets;
|
||||
use crate::WASM_PAGE_SIZE;
|
||||
use alloc::vec::Vec;
|
||||
use core::clone::Clone;
|
||||
use core::convert::TryFrom;
|
||||
use cranelift_codegen::cursor::FuncCursor;
|
||||
use cranelift_codegen::ir;
|
||||
use cranelift_codegen::ir::condcodes::*;
|
||||
use cranelift_codegen::ir::immediates::{Offset32, Uimm64};
|
||||
use cranelift_codegen::ir::types::*;
|
||||
use cranelift_codegen::ir::{AbiParam, ArgumentPurpose, Function, InstBuilder, Signature};
|
||||
use cranelift_codegen::isa::TargetFrontendConfig;
|
||||
use cranelift_entity::EntityRef;
|
||||
use cranelift_wasm::{
|
||||
self, FuncIndex, GlobalIndex, GlobalVariable, MemoryIndex, SignatureIndex, TableIndex,
|
||||
WasmResult,
|
||||
};
|
||||
#[cfg(feature = "lightbeam")]
|
||||
use cranelift_wasm::{DefinedFuncIndex, DefinedGlobalIndex, DefinedMemoryIndex, DefinedTableIndex};
|
||||
|
||||
/// Compute an `ir::ExternalName` for a given wasm function index.
|
||||
pub fn get_func_name(func_index: FuncIndex) -> ir::ExternalName {
|
||||
ir::ExternalName::user(0, func_index.as_u32())
|
||||
}
|
||||
|
||||
/// Compute an `ir::ExternalName` for the `memory.grow` libcall for
|
||||
/// 32-bit locally-defined memories.
|
||||
pub fn get_memory32_grow_name() -> ir::ExternalName {
|
||||
ir::ExternalName::user(1, 0)
|
||||
}
|
||||
|
||||
/// Compute an `ir::ExternalName` for the `memory.grow` libcall for
|
||||
/// 32-bit imported memories.
|
||||
pub fn get_imported_memory32_grow_name() -> ir::ExternalName {
|
||||
ir::ExternalName::user(1, 1)
|
||||
}
|
||||
|
||||
/// Compute an `ir::ExternalName` for the `memory.size` libcall for
|
||||
/// 32-bit locally-defined memories.
|
||||
pub fn get_memory32_size_name() -> ir::ExternalName {
|
||||
ir::ExternalName::user(1, 2)
|
||||
}
|
||||
|
||||
/// Compute an `ir::ExternalName` for the `memory.size` libcall for
|
||||
/// 32-bit imported memories.
|
||||
pub fn get_imported_memory32_size_name() -> ir::ExternalName {
|
||||
ir::ExternalName::user(1, 3)
|
||||
}
|
||||
|
||||
/// An index type for builtin functions.
|
||||
pub struct BuiltinFunctionIndex(u32);
|
||||
|
||||
impl BuiltinFunctionIndex {
|
||||
/// Returns an index for wasm's `memory.grow` builtin function.
|
||||
pub const fn get_memory32_grow_index() -> Self {
|
||||
Self(0)
|
||||
}
|
||||
/// Returns an index for wasm's imported `memory.grow` builtin function.
|
||||
pub const fn get_imported_memory32_grow_index() -> Self {
|
||||
Self(1)
|
||||
}
|
||||
/// Returns an index for wasm's `memory.size` builtin function.
|
||||
pub const fn get_memory32_size_index() -> Self {
|
||||
Self(2)
|
||||
}
|
||||
/// Returns an index for wasm's imported `memory.size` builtin function.
|
||||
pub const fn get_imported_memory32_size_index() -> Self {
|
||||
Self(3)
|
||||
}
|
||||
/// Returns the total number of builtin functions.
|
||||
pub const fn builtin_functions_total_number() -> u32 {
|
||||
4
|
||||
}
|
||||
|
||||
/// Return the index as an u32 number.
|
||||
pub const fn index(&self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// The `FuncEnvironment` implementation for use by the `ModuleEnvironment`.
|
||||
pub struct FuncEnvironment<'module_environment> {
|
||||
/// Target-specified configuration.
|
||||
target_config: TargetFrontendConfig,
|
||||
|
||||
/// The module-level environment which this function-level environment belongs to.
|
||||
module: &'module_environment Module,
|
||||
|
||||
/// The Cranelift global holding the vmctx address.
|
||||
vmctx: Option<ir::GlobalValue>,
|
||||
|
||||
/// The external function signature for implementing wasm's `memory.size`
|
||||
/// for locally-defined 32-bit memories.
|
||||
memory32_size_sig: Option<ir::SigRef>,
|
||||
|
||||
/// The external function signature for implementing wasm's `memory.grow`
|
||||
/// for locally-defined memories.
|
||||
memory_grow_sig: Option<ir::SigRef>,
|
||||
|
||||
/// Offsets to struct fields accessed by JIT code.
|
||||
offsets: VMOffsets,
|
||||
}
|
||||
|
||||
impl<'module_environment> FuncEnvironment<'module_environment> {
|
||||
pub fn new(target_config: TargetFrontendConfig, module: &'module_environment Module) -> Self {
|
||||
Self {
|
||||
target_config,
|
||||
module,
|
||||
vmctx: None,
|
||||
memory32_size_sig: None,
|
||||
memory_grow_sig: None,
|
||||
offsets: VMOffsets::new(target_config.pointer_bytes(), module),
|
||||
}
|
||||
}
|
||||
|
||||
fn pointer_type(&self) -> ir::Type {
|
||||
self.target_config.pointer_type()
|
||||
}
|
||||
|
||||
fn vmctx(&mut self, func: &mut Function) -> ir::GlobalValue {
|
||||
self.vmctx.unwrap_or_else(|| {
|
||||
let vmctx = func.create_global_value(ir::GlobalValueData::VMContext);
|
||||
self.vmctx = Some(vmctx);
|
||||
vmctx
|
||||
})
|
||||
}
|
||||
|
||||
fn get_memory_grow_sig(&mut self, func: &mut Function) -> ir::SigRef {
|
||||
let sig = self.memory_grow_sig.unwrap_or_else(|| {
|
||||
func.import_signature(Signature {
|
||||
params: vec![
|
||||
AbiParam::special(self.pointer_type(), ArgumentPurpose::VMContext),
|
||||
AbiParam::new(I32),
|
||||
AbiParam::new(I32),
|
||||
],
|
||||
returns: vec![AbiParam::new(I32)],
|
||||
call_conv: self.target_config.default_call_conv,
|
||||
})
|
||||
});
|
||||
self.memory_grow_sig = Some(sig);
|
||||
sig
|
||||
}
|
||||
|
||||
/// Return the memory.grow function signature to call for the given index, along with the
|
||||
/// translated index value to pass to it and its index in `VMBuiltinFunctionsArray`.
|
||||
fn get_memory_grow_func(
|
||||
&mut self,
|
||||
func: &mut Function,
|
||||
index: MemoryIndex,
|
||||
) -> (ir::SigRef, usize, BuiltinFunctionIndex) {
|
||||
if self.module.is_imported_memory(index) {
|
||||
(
|
||||
self.get_memory_grow_sig(func),
|
||||
index.index(),
|
||||
BuiltinFunctionIndex::get_imported_memory32_grow_index(),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
self.get_memory_grow_sig(func),
|
||||
self.module.defined_memory_index(index).unwrap().index(),
|
||||
BuiltinFunctionIndex::get_memory32_grow_index(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_memory32_size_sig(&mut self, func: &mut Function) -> ir::SigRef {
|
||||
let sig = self.memory32_size_sig.unwrap_or_else(|| {
|
||||
func.import_signature(Signature {
|
||||
params: vec![
|
||||
AbiParam::special(self.pointer_type(), ArgumentPurpose::VMContext),
|
||||
AbiParam::new(I32),
|
||||
],
|
||||
returns: vec![AbiParam::new(I32)],
|
||||
call_conv: self.target_config.default_call_conv,
|
||||
})
|
||||
});
|
||||
self.memory32_size_sig = Some(sig);
|
||||
sig
|
||||
}
|
||||
|
||||
/// Return the memory.size function signature to call for the given index, along with the
|
||||
/// translated index value to pass to it and its index in `VMBuiltinFunctionsArray`.
|
||||
fn get_memory_size_func(
|
||||
&mut self,
|
||||
func: &mut Function,
|
||||
index: MemoryIndex,
|
||||
) -> (ir::SigRef, usize, BuiltinFunctionIndex) {
|
||||
if self.module.is_imported_memory(index) {
|
||||
(
|
||||
self.get_memory32_size_sig(func),
|
||||
index.index(),
|
||||
BuiltinFunctionIndex::get_imported_memory32_size_index(),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
self.get_memory32_size_sig(func),
|
||||
self.module.defined_memory_index(index).unwrap().index(),
|
||||
BuiltinFunctionIndex::get_memory32_size_index(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Translates load of builtin function and returns a pair of values `vmctx`
|
||||
/// and address of the loaded function.
|
||||
fn translate_load_builtin_function_address(
|
||||
&mut self,
|
||||
pos: &mut FuncCursor<'_>,
|
||||
callee_func_idx: BuiltinFunctionIndex,
|
||||
) -> (ir::Value, ir::Value) {
|
||||
// We use an indirect call so that we don't have to patch the code at runtime.
|
||||
let pointer_type = self.pointer_type();
|
||||
let vmctx = self.vmctx(&mut pos.func);
|
||||
let base = pos.ins().global_value(pointer_type, vmctx);
|
||||
|
||||
let mut mem_flags = ir::MemFlags::trusted();
|
||||
mem_flags.set_readonly();
|
||||
|
||||
// Load the callee address.
|
||||
let body_offset =
|
||||
i32::try_from(self.offsets.vmctx_builtin_function(callee_func_idx)).unwrap();
|
||||
let func_addr = pos.ins().load(pointer_type, mem_flags, base, body_offset);
|
||||
|
||||
(base, func_addr)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "lightbeam")]
|
||||
impl lightbeam::ModuleContext for FuncEnvironment<'_> {
|
||||
type Signature = ir::Signature;
|
||||
type GlobalType = ir::Type;
|
||||
|
||||
fn func_index(&self, defined_func_index: u32) -> u32 {
|
||||
self.module
|
||||
.func_index(DefinedFuncIndex::from_u32(defined_func_index))
|
||||
.as_u32()
|
||||
}
|
||||
|
||||
fn defined_func_index(&self, func_index: u32) -> Option<u32> {
|
||||
self.module
|
||||
.defined_func_index(FuncIndex::from_u32(func_index))
|
||||
.map(DefinedFuncIndex::as_u32)
|
||||
}
|
||||
|
||||
fn defined_global_index(&self, global_index: u32) -> Option<u32> {
|
||||
self.module
|
||||
.defined_global_index(GlobalIndex::from_u32(global_index))
|
||||
.map(DefinedGlobalIndex::as_u32)
|
||||
}
|
||||
|
||||
fn global_type(&self, global_index: u32) -> &Self::GlobalType {
|
||||
&self.module.globals[GlobalIndex::from_u32(global_index)].ty
|
||||
}
|
||||
|
||||
fn func_type_index(&self, func_idx: u32) -> u32 {
|
||||
self.module.functions[FuncIndex::from_u32(func_idx)].as_u32()
|
||||
}
|
||||
|
||||
fn signature(&self, index: u32) -> &Self::Signature {
|
||||
&self.module.signatures[SignatureIndex::from_u32(index)]
|
||||
}
|
||||
|
||||
fn defined_table_index(&self, table_index: u32) -> Option<u32> {
|
||||
self.module
|
||||
.defined_table_index(TableIndex::from_u32(table_index))
|
||||
.map(DefinedTableIndex::as_u32)
|
||||
}
|
||||
|
||||
fn defined_memory_index(&self, memory_index: u32) -> Option<u32> {
|
||||
self.module
|
||||
.defined_memory_index(MemoryIndex::from_u32(memory_index))
|
||||
.map(DefinedMemoryIndex::as_u32)
|
||||
}
|
||||
|
||||
fn vmctx_vmfunction_import_body(&self, func_index: u32) -> u32 {
|
||||
self.offsets
|
||||
.vmctx_vmfunction_import_body(FuncIndex::from_u32(func_index))
|
||||
}
|
||||
fn vmctx_vmfunction_import_vmctx(&self, func_index: u32) -> u32 {
|
||||
self.offsets
|
||||
.vmctx_vmfunction_import_vmctx(FuncIndex::from_u32(func_index))
|
||||
}
|
||||
|
||||
fn vmctx_vmglobal_import_from(&self, global_index: u32) -> u32 {
|
||||
self.offsets
|
||||
.vmctx_vmglobal_import_from(GlobalIndex::from_u32(global_index))
|
||||
}
|
||||
fn vmctx_vmglobal_definition(&self, defined_global_index: u32) -> u32 {
|
||||
self.offsets
|
||||
.vmctx_vmglobal_definition(DefinedGlobalIndex::from_u32(defined_global_index))
|
||||
}
|
||||
fn vmctx_vmmemory_import_from(&self, memory_index: u32) -> u32 {
|
||||
self.offsets
|
||||
.vmctx_vmmemory_import_from(MemoryIndex::from_u32(memory_index))
|
||||
}
|
||||
fn vmctx_vmmemory_definition(&self, defined_memory_index: u32) -> u32 {
|
||||
self.offsets
|
||||
.vmctx_vmmemory_definition(DefinedMemoryIndex::from_u32(defined_memory_index))
|
||||
}
|
||||
fn vmctx_vmmemory_definition_base(&self, defined_memory_index: u32) -> u32 {
|
||||
self.offsets
|
||||
.vmctx_vmmemory_definition_base(DefinedMemoryIndex::from_u32(defined_memory_index))
|
||||
}
|
||||
fn vmctx_vmmemory_definition_current_length(&self, defined_memory_index: u32) -> u32 {
|
||||
self.offsets
|
||||
.vmctx_vmmemory_definition_current_length(DefinedMemoryIndex::from_u32(
|
||||
defined_memory_index,
|
||||
))
|
||||
}
|
||||
fn vmmemory_definition_base(&self) -> u8 {
|
||||
self.offsets.vmmemory_definition_base()
|
||||
}
|
||||
fn vmmemory_definition_current_length(&self) -> u8 {
|
||||
self.offsets.vmmemory_definition_current_length()
|
||||
}
|
||||
fn vmctx_vmtable_import_from(&self, table_index: u32) -> u32 {
|
||||
self.offsets
|
||||
.vmctx_vmtable_import_from(TableIndex::from_u32(table_index))
|
||||
}
|
||||
fn vmctx_vmtable_definition(&self, defined_table_index: u32) -> u32 {
|
||||
self.offsets
|
||||
.vmctx_vmtable_definition(DefinedTableIndex::from_u32(defined_table_index))
|
||||
}
|
||||
fn vmctx_vmtable_definition_base(&self, defined_table_index: u32) -> u32 {
|
||||
self.offsets
|
||||
.vmctx_vmtable_definition_base(DefinedTableIndex::from_u32(defined_table_index))
|
||||
}
|
||||
fn vmctx_vmtable_definition_current_elements(&self, defined_table_index: u32) -> u32 {
|
||||
self.offsets
|
||||
.vmctx_vmtable_definition_current_elements(DefinedTableIndex::from_u32(
|
||||
defined_table_index,
|
||||
))
|
||||
}
|
||||
fn vmtable_definition_base(&self) -> u8 {
|
||||
self.offsets.vmtable_definition_base()
|
||||
}
|
||||
fn vmtable_definition_current_elements(&self) -> u8 {
|
||||
self.offsets.vmtable_definition_current_elements()
|
||||
}
|
||||
fn vmcaller_checked_anyfunc_type_index(&self) -> u8 {
|
||||
self.offsets.vmcaller_checked_anyfunc_type_index()
|
||||
}
|
||||
fn vmcaller_checked_anyfunc_func_ptr(&self) -> u8 {
|
||||
self.offsets.vmcaller_checked_anyfunc_func_ptr()
|
||||
}
|
||||
fn vmcaller_checked_anyfunc_vmctx(&self) -> u8 {
|
||||
self.offsets.vmcaller_checked_anyfunc_vmctx()
|
||||
}
|
||||
fn size_of_vmcaller_checked_anyfunc(&self) -> u8 {
|
||||
self.offsets.size_of_vmcaller_checked_anyfunc()
|
||||
}
|
||||
fn vmctx_vmshared_signature_id(&self, signature_idx: u32) -> u32 {
|
||||
self.offsets
|
||||
.vmctx_vmshared_signature_id(SignatureIndex::from_u32(signature_idx))
|
||||
}
|
||||
|
||||
// TODO: type of a global
|
||||
}
|
||||
|
||||
impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'module_environment> {
|
||||
fn target_config(&self) -> TargetFrontendConfig {
|
||||
self.target_config
|
||||
}
|
||||
|
||||
fn make_table(&mut self, func: &mut ir::Function, index: TableIndex) -> WasmResult<ir::Table> {
|
||||
let pointer_type = self.pointer_type();
|
||||
|
||||
let (ptr, base_offset, current_elements_offset) = {
|
||||
let vmctx = self.vmctx(func);
|
||||
if let Some(def_index) = self.module.defined_table_index(index) {
|
||||
let base_offset =
|
||||
i32::try_from(self.offsets.vmctx_vmtable_definition_base(def_index)).unwrap();
|
||||
let current_elements_offset = i32::try_from(
|
||||
self.offsets
|
||||
.vmctx_vmtable_definition_current_elements(def_index),
|
||||
)
|
||||
.unwrap();
|
||||
(vmctx, base_offset, current_elements_offset)
|
||||
} else {
|
||||
let from_offset = self.offsets.vmctx_vmtable_import_from(index);
|
||||
let table = func.create_global_value(ir::GlobalValueData::Load {
|
||||
base: vmctx,
|
||||
offset: Offset32::new(i32::try_from(from_offset).unwrap()),
|
||||
global_type: pointer_type,
|
||||
readonly: true,
|
||||
});
|
||||
let base_offset = i32::from(self.offsets.vmtable_definition_base());
|
||||
let current_elements_offset =
|
||||
i32::from(self.offsets.vmtable_definition_current_elements());
|
||||
(table, base_offset, current_elements_offset)
|
||||
}
|
||||
};
|
||||
|
||||
let base_gv = func.create_global_value(ir::GlobalValueData::Load {
|
||||
base: ptr,
|
||||
offset: Offset32::new(base_offset),
|
||||
global_type: pointer_type,
|
||||
readonly: false,
|
||||
});
|
||||
let bound_gv = func.create_global_value(ir::GlobalValueData::Load {
|
||||
base: ptr,
|
||||
offset: Offset32::new(current_elements_offset),
|
||||
global_type: self.offsets.type_of_vmtable_definition_current_elements(),
|
||||
readonly: false,
|
||||
});
|
||||
|
||||
let element_size = match self.module.table_plans[index].style {
|
||||
TableStyle::CallerChecksSignature => {
|
||||
u64::from(self.offsets.size_of_vmcaller_checked_anyfunc())
|
||||
}
|
||||
};
|
||||
|
||||
Ok(func.create_table(ir::TableData {
|
||||
base_gv,
|
||||
min_size: Uimm64::new(0),
|
||||
bound_gv,
|
||||
element_size: Uimm64::new(element_size),
|
||||
index_type: I32,
|
||||
}))
|
||||
}
|
||||
|
||||
fn make_heap(&mut self, func: &mut ir::Function, index: MemoryIndex) -> WasmResult<ir::Heap> {
|
||||
let pointer_type = self.pointer_type();
|
||||
|
||||
let (ptr, base_offset, current_length_offset) = {
|
||||
let vmctx = self.vmctx(func);
|
||||
if let Some(def_index) = self.module.defined_memory_index(index) {
|
||||
let base_offset =
|
||||
i32::try_from(self.offsets.vmctx_vmmemory_definition_base(def_index)).unwrap();
|
||||
let current_length_offset = i32::try_from(
|
||||
self.offsets
|
||||
.vmctx_vmmemory_definition_current_length(def_index),
|
||||
)
|
||||
.unwrap();
|
||||
(vmctx, base_offset, current_length_offset)
|
||||
} else {
|
||||
let from_offset = self.offsets.vmctx_vmmemory_import_from(index);
|
||||
let memory = func.create_global_value(ir::GlobalValueData::Load {
|
||||
base: vmctx,
|
||||
offset: Offset32::new(i32::try_from(from_offset).unwrap()),
|
||||
global_type: pointer_type,
|
||||
readonly: true,
|
||||
});
|
||||
let base_offset = i32::from(self.offsets.vmmemory_definition_base());
|
||||
let current_length_offset =
|
||||
i32::from(self.offsets.vmmemory_definition_current_length());
|
||||
(memory, base_offset, current_length_offset)
|
||||
}
|
||||
};
|
||||
|
||||
// If we have a declared maximum, we can make this a "static" heap, which is
|
||||
// allocated up front and never moved.
|
||||
let (offset_guard_size, heap_style, readonly_base) = match self.module.memory_plans[index] {
|
||||
MemoryPlan {
|
||||
memory: _,
|
||||
style: MemoryStyle::Dynamic,
|
||||
offset_guard_size,
|
||||
} => {
|
||||
let heap_bound = func.create_global_value(ir::GlobalValueData::Load {
|
||||
base: ptr,
|
||||
offset: Offset32::new(current_length_offset),
|
||||
global_type: self.offsets.type_of_vmmemory_definition_current_length(),
|
||||
readonly: false,
|
||||
});
|
||||
(
|
||||
Uimm64::new(offset_guard_size),
|
||||
ir::HeapStyle::Dynamic {
|
||||
bound_gv: heap_bound,
|
||||
},
|
||||
false,
|
||||
)
|
||||
}
|
||||
MemoryPlan {
|
||||
memory: _,
|
||||
style: MemoryStyle::Static { bound },
|
||||
offset_guard_size,
|
||||
} => (
|
||||
Uimm64::new(offset_guard_size),
|
||||
ir::HeapStyle::Static {
|
||||
bound: Uimm64::new(u64::from(bound) * u64::from(WASM_PAGE_SIZE)),
|
||||
},
|
||||
true,
|
||||
),
|
||||
};
|
||||
|
||||
let heap_base = func.create_global_value(ir::GlobalValueData::Load {
|
||||
base: ptr,
|
||||
offset: Offset32::new(base_offset),
|
||||
global_type: pointer_type,
|
||||
readonly: readonly_base,
|
||||
});
|
||||
Ok(func.create_heap(ir::HeapData {
|
||||
base: heap_base,
|
||||
min_size: 0.into(),
|
||||
offset_guard_size,
|
||||
style: heap_style,
|
||||
index_type: I32,
|
||||
}))
|
||||
}
|
||||
|
||||
fn make_global(
|
||||
&mut self,
|
||||
func: &mut ir::Function,
|
||||
index: GlobalIndex,
|
||||
) -> WasmResult<GlobalVariable> {
|
||||
let pointer_type = self.pointer_type();
|
||||
|
||||
let (ptr, offset) = {
|
||||
let vmctx = self.vmctx(func);
|
||||
if let Some(def_index) = self.module.defined_global_index(index) {
|
||||
let offset =
|
||||
i32::try_from(self.offsets.vmctx_vmglobal_definition(def_index)).unwrap();
|
||||
(vmctx, offset)
|
||||
} else {
|
||||
let from_offset = self.offsets.vmctx_vmglobal_import_from(index);
|
||||
let global = func.create_global_value(ir::GlobalValueData::Load {
|
||||
base: vmctx,
|
||||
offset: Offset32::new(i32::try_from(from_offset).unwrap()),
|
||||
global_type: pointer_type,
|
||||
readonly: true,
|
||||
});
|
||||
(global, 0)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(GlobalVariable::Memory {
|
||||
gv: ptr,
|
||||
offset: offset.into(),
|
||||
ty: self.module.globals[index].ty,
|
||||
})
|
||||
}
|
||||
|
||||
fn make_indirect_sig(
|
||||
&mut self,
|
||||
func: &mut ir::Function,
|
||||
index: SignatureIndex,
|
||||
) -> WasmResult<ir::SigRef> {
|
||||
Ok(func.import_signature(self.module.signatures[index].clone()))
|
||||
}
|
||||
|
||||
fn make_direct_func(
|
||||
&mut self,
|
||||
func: &mut ir::Function,
|
||||
index: FuncIndex,
|
||||
) -> WasmResult<ir::FuncRef> {
|
||||
let sigidx = self.module.functions[index];
|
||||
let signature = func.import_signature(self.module.signatures[sigidx].clone());
|
||||
let name = get_func_name(index);
|
||||
Ok(func.import_function(ir::ExtFuncData {
|
||||
name,
|
||||
signature,
|
||||
// We currently allocate all code segments independently, so nothing
|
||||
// is colocated.
|
||||
colocated: false,
|
||||
}))
|
||||
}
|
||||
|
||||
fn translate_call_indirect(
|
||||
&mut self,
|
||||
mut pos: FuncCursor<'_>,
|
||||
table_index: TableIndex,
|
||||
table: ir::Table,
|
||||
sig_index: SignatureIndex,
|
||||
sig_ref: ir::SigRef,
|
||||
callee: ir::Value,
|
||||
call_args: &[ir::Value],
|
||||
) -> WasmResult<ir::Inst> {
|
||||
let pointer_type = self.pointer_type();
|
||||
|
||||
let table_entry_addr = pos.ins().table_addr(pointer_type, table, callee, 0);
|
||||
|
||||
// Dereference table_entry_addr to get the function address.
|
||||
let mem_flags = ir::MemFlags::trusted();
|
||||
let func_addr = pos.ins().load(
|
||||
pointer_type,
|
||||
mem_flags,
|
||||
table_entry_addr,
|
||||
i32::from(self.offsets.vmcaller_checked_anyfunc_func_ptr()),
|
||||
);
|
||||
|
||||
// Check whether `func_addr` is null.
|
||||
pos.ins().trapz(func_addr, ir::TrapCode::IndirectCallToNull);
|
||||
|
||||
// If necessary, check the signature.
|
||||
match self.module.table_plans[table_index].style {
|
||||
TableStyle::CallerChecksSignature => {
|
||||
let sig_id_size = self.offsets.size_of_vmshared_signature_index();
|
||||
let sig_id_type = Type::int(u16::from(sig_id_size) * 8).unwrap();
|
||||
let vmctx = self.vmctx(pos.func);
|
||||
let base = pos.ins().global_value(pointer_type, vmctx);
|
||||
let offset =
|
||||
i32::try_from(self.offsets.vmctx_vmshared_signature_id(sig_index)).unwrap();
|
||||
|
||||
// Load the caller ID.
|
||||
let mut mem_flags = ir::MemFlags::trusted();
|
||||
mem_flags.set_readonly();
|
||||
let caller_sig_id = pos.ins().load(sig_id_type, mem_flags, base, offset);
|
||||
|
||||
// Load the callee ID.
|
||||
let mem_flags = ir::MemFlags::trusted();
|
||||
let callee_sig_id = pos.ins().load(
|
||||
sig_id_type,
|
||||
mem_flags,
|
||||
table_entry_addr,
|
||||
i32::from(self.offsets.vmcaller_checked_anyfunc_type_index()),
|
||||
);
|
||||
|
||||
// Check that they match.
|
||||
let cmp = pos.ins().icmp(IntCC::Equal, callee_sig_id, caller_sig_id);
|
||||
pos.ins().trapz(cmp, ir::TrapCode::BadSignature);
|
||||
}
|
||||
}
|
||||
|
||||
let mut real_call_args = Vec::with_capacity(call_args.len() + 1);
|
||||
|
||||
// First append the callee vmctx address.
|
||||
let vmctx = pos.ins().load(
|
||||
pointer_type,
|
||||
mem_flags,
|
||||
table_entry_addr,
|
||||
i32::from(self.offsets.vmcaller_checked_anyfunc_vmctx()),
|
||||
);
|
||||
real_call_args.push(vmctx);
|
||||
|
||||
// Then append the regular call arguments.
|
||||
real_call_args.extend_from_slice(call_args);
|
||||
|
||||
Ok(pos.ins().call_indirect(sig_ref, func_addr, &real_call_args))
|
||||
}
|
||||
|
||||
fn translate_call(
|
||||
&mut self,
|
||||
mut pos: FuncCursor<'_>,
|
||||
callee_index: FuncIndex,
|
||||
callee: ir::FuncRef,
|
||||
call_args: &[ir::Value],
|
||||
) -> WasmResult<ir::Inst> {
|
||||
let mut real_call_args = Vec::with_capacity(call_args.len() + 1);
|
||||
|
||||
// Handle direct calls to locally-defined functions.
|
||||
if !self.module.is_imported_function(callee_index) {
|
||||
// First append the callee vmctx address.
|
||||
real_call_args.push(pos.func.special_param(ArgumentPurpose::VMContext).unwrap());
|
||||
|
||||
// Then append the regular call arguments.
|
||||
real_call_args.extend_from_slice(call_args);
|
||||
|
||||
return Ok(pos.ins().call(callee, &real_call_args));
|
||||
}
|
||||
|
||||
// Handle direct calls to imported functions. We use an indirect call
|
||||
// so that we don't have to patch the code at runtime.
|
||||
let pointer_type = self.pointer_type();
|
||||
let sig_ref = pos.func.dfg.ext_funcs[callee].signature;
|
||||
let vmctx = self.vmctx(&mut pos.func);
|
||||
let base = pos.ins().global_value(pointer_type, vmctx);
|
||||
|
||||
let mem_flags = ir::MemFlags::trusted();
|
||||
|
||||
// Load the callee address.
|
||||
let body_offset =
|
||||
i32::try_from(self.offsets.vmctx_vmfunction_import_body(callee_index)).unwrap();
|
||||
let func_addr = pos.ins().load(pointer_type, mem_flags, base, body_offset);
|
||||
|
||||
// First append the callee vmctx address.
|
||||
let vmctx_offset =
|
||||
i32::try_from(self.offsets.vmctx_vmfunction_import_vmctx(callee_index)).unwrap();
|
||||
let vmctx = pos.ins().load(pointer_type, mem_flags, base, vmctx_offset);
|
||||
real_call_args.push(vmctx);
|
||||
|
||||
// Then append the regular call arguments.
|
||||
real_call_args.extend_from_slice(call_args);
|
||||
|
||||
Ok(pos.ins().call_indirect(sig_ref, func_addr, &real_call_args))
|
||||
}
|
||||
|
||||
fn translate_memory_grow(
|
||||
&mut self,
|
||||
mut pos: FuncCursor<'_>,
|
||||
index: MemoryIndex,
|
||||
_heap: ir::Heap,
|
||||
val: ir::Value,
|
||||
) -> WasmResult<ir::Value> {
|
||||
let (func_sig, index_arg, func_idx) = self.get_memory_grow_func(&mut pos.func, index);
|
||||
let memory_index = pos.ins().iconst(I32, index_arg as i64);
|
||||
let (vmctx, func_addr) = self.translate_load_builtin_function_address(&mut pos, func_idx);
|
||||
let call_inst = pos
|
||||
.ins()
|
||||
.call_indirect(func_sig, func_addr, &[vmctx, val, memory_index]);
|
||||
Ok(*pos.func.dfg.inst_results(call_inst).first().unwrap())
|
||||
}
|
||||
|
||||
fn translate_memory_size(
|
||||
&mut self,
|
||||
mut pos: FuncCursor<'_>,
|
||||
index: MemoryIndex,
|
||||
_heap: ir::Heap,
|
||||
) -> WasmResult<ir::Value> {
|
||||
let (func_sig, index_arg, func_idx) = self.get_memory_size_func(&mut pos.func, index);
|
||||
let memory_index = pos.ins().iconst(I32, index_arg as i64);
|
||||
let (vmctx, func_addr) = self.translate_load_builtin_function_address(&mut pos, func_idx);
|
||||
let call_inst = pos
|
||||
.ins()
|
||||
.call_indirect(func_sig, func_addr, &[vmctx, memory_index]);
|
||||
Ok(*pos.func.dfg.inst_results(call_inst).first().unwrap())
|
||||
}
|
||||
}
|
||||
73
crates/environ/src/lib.rs
Normal file
73
crates/environ/src/lib.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
//! Standalone environment for WebAssembly using Cranelift. Provides functions to translate
|
||||
//! `get_global`, `set_global`, `memory.size`, `memory.grow`, `call_indirect` that hardcode in
|
||||
//! the translation the base addresses of regions of memory that will hold the globals, tables and
|
||||
//! linear memories.
|
||||
|
||||
#![deny(missing_docs, trivial_numeric_casts, unused_extern_crates)]
|
||||
#![warn(unused_import_braces)]
|
||||
#![cfg_attr(feature = "std", deny(unstable_features))]
|
||||
#![cfg_attr(feature = "clippy", plugin(clippy(conf_file = "../../clippy.toml")))]
|
||||
#![cfg_attr(
|
||||
feature = "cargo-clippy",
|
||||
allow(clippy::new_without_default, clippy::new_without_default_derive)
|
||||
)]
|
||||
#![cfg_attr(
|
||||
feature = "cargo-clippy",
|
||||
warn(
|
||||
clippy::float_arithmetic,
|
||||
clippy::mut_mut,
|
||||
clippy::nonminimal_bool,
|
||||
clippy::option_map_unwrap_or,
|
||||
clippy::option_map_unwrap_or_else,
|
||||
clippy::print_stdout,
|
||||
clippy::unicode_not_nfc,
|
||||
clippy::use_self
|
||||
)
|
||||
)]
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
mod address_map;
|
||||
mod compilation;
|
||||
mod func_environ;
|
||||
mod module;
|
||||
mod module_environ;
|
||||
mod tunables;
|
||||
mod vmoffsets;
|
||||
|
||||
mod cache;
|
||||
|
||||
pub mod cranelift;
|
||||
#[cfg(feature = "lightbeam")]
|
||||
pub mod lightbeam;
|
||||
|
||||
pub use crate::address_map::{
|
||||
FunctionAddressMap, InstructionAddressMap, ModuleAddressMap, ModuleVmctxInfo, ValueLabelsRanges,
|
||||
};
|
||||
pub use crate::cache::{create_new_config as cache_create_new_config, init as cache_init};
|
||||
pub use crate::compilation::{
|
||||
Compilation, CompileError, CompiledFunction, Compiler, Relocation, RelocationTarget,
|
||||
Relocations, TrapInformation, Traps,
|
||||
};
|
||||
pub use crate::cranelift::Cranelift;
|
||||
pub use crate::func_environ::BuiltinFunctionIndex;
|
||||
#[cfg(feature = "lightbeam")]
|
||||
pub use crate::lightbeam::Lightbeam;
|
||||
pub use crate::module::{
|
||||
Export, MemoryPlan, MemoryStyle, Module, TableElements, TablePlan, TableStyle,
|
||||
};
|
||||
pub use crate::module_environ::{
|
||||
translate_signature, DataInitializer, DataInitializerLocation, FunctionBodyData,
|
||||
ModuleEnvironment, ModuleTranslation,
|
||||
};
|
||||
pub use crate::tunables::Tunables;
|
||||
pub use crate::vmoffsets::{TargetSharedSignatureIndex, VMOffsets};
|
||||
|
||||
/// WebAssembly page sizes are defined to be 64KiB.
|
||||
pub const WASM_PAGE_SIZE: u32 = 0x10000;
|
||||
|
||||
/// The number of pages we can have before we run out of byte index space.
|
||||
pub const WASM_MAX_PAGES: u32 = 0x10000;
|
||||
|
||||
/// Version number of this crate.
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
83
crates/environ/src/lightbeam.rs
Normal file
83
crates/environ/src/lightbeam.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
//! Support for compiling with Lightbeam.
|
||||
|
||||
use crate::compilation::{Compilation, CompileError, Relocations, Traps};
|
||||
use crate::func_environ::FuncEnvironment;
|
||||
use crate::module::Module;
|
||||
use crate::module_environ::FunctionBodyData;
|
||||
// TODO: Put this in `compilation`
|
||||
use crate::address_map::{ModuleAddressMap, ValueLabelsRanges};
|
||||
use crate::cranelift::RelocSink;
|
||||
use cranelift_codegen::{ir, isa};
|
||||
use cranelift_entity::{PrimaryMap, SecondaryMap};
|
||||
use cranelift_wasm::{DefinedFuncIndex, ModuleTranslationState};
|
||||
use lightbeam;
|
||||
|
||||
/// A compiler that compiles a WebAssembly module with Lightbeam, directly translating the Wasm file.
|
||||
pub struct Lightbeam;
|
||||
|
||||
impl crate::compilation::Compiler for Lightbeam {
|
||||
/// Compile the module using Lightbeam, producing a compilation result with
|
||||
/// associated relocations.
|
||||
fn compile_module<'data, 'module>(
|
||||
module: &'module Module,
|
||||
_module_translation: &ModuleTranslationState,
|
||||
function_body_inputs: PrimaryMap<DefinedFuncIndex, FunctionBodyData<'data>>,
|
||||
isa: &dyn isa::TargetIsa,
|
||||
// TODO
|
||||
generate_debug_info: bool,
|
||||
) -> Result<
|
||||
(
|
||||
Compilation,
|
||||
Relocations,
|
||||
ModuleAddressMap,
|
||||
ValueLabelsRanges,
|
||||
PrimaryMap<DefinedFuncIndex, ir::StackSlots>,
|
||||
Traps,
|
||||
),
|
||||
CompileError,
|
||||
> {
|
||||
if generate_debug_info {
|
||||
return Err(CompileError::DebugInfoNotSupported);
|
||||
}
|
||||
|
||||
let env = FuncEnvironment::new(isa.frontend_config(), module);
|
||||
let mut relocations = PrimaryMap::new();
|
||||
let mut codegen_session: lightbeam::CodeGenSession<_> =
|
||||
lightbeam::CodeGenSession::new(function_body_inputs.len() as u32, &env);
|
||||
|
||||
for (i, function_body) in &function_body_inputs {
|
||||
let func_index = module.func_index(i);
|
||||
let mut reloc_sink = RelocSink::new(func_index);
|
||||
|
||||
lightbeam::translate_function(
|
||||
&mut codegen_session,
|
||||
&mut reloc_sink,
|
||||
i.as_u32(),
|
||||
&lightbeam::wasmparser::FunctionBody::new(0, function_body.data),
|
||||
)
|
||||
.expect("Failed to translate function. TODO: Stop this from panicking");
|
||||
relocations.push(reloc_sink.func_relocs);
|
||||
}
|
||||
|
||||
let code_section = codegen_session
|
||||
.into_translated_code_section()
|
||||
.expect("Failed to generate output code. TODO: Stop this from panicking");
|
||||
|
||||
// TODO pass jump table offsets to Compilation::from_buffer() when they
|
||||
// are implemented in lightbeam -- using empty set of offsets for now.
|
||||
// TODO: pass an empty range for the unwind information until lightbeam emits it
|
||||
let code_section_ranges_and_jt = code_section
|
||||
.funcs()
|
||||
.into_iter()
|
||||
.map(|r| (r, SecondaryMap::new(), 0..0));
|
||||
|
||||
Ok((
|
||||
Compilation::from_buffer(code_section.buffer().to_vec(), code_section_ranges_and_jt),
|
||||
relocations,
|
||||
ModuleAddressMap::new(),
|
||||
ValueLabelsRanges::new(),
|
||||
PrimaryMap::new(),
|
||||
Traps::new(),
|
||||
))
|
||||
}
|
||||
}
|
||||
306
crates/environ/src/module.rs
Normal file
306
crates/environ/src/module.rs
Normal file
@@ -0,0 +1,306 @@
|
||||
//! Data structures for representing decoded wasm modules.
|
||||
|
||||
use crate::module_environ::FunctionBodyData;
|
||||
use crate::tunables::Tunables;
|
||||
use alloc::boxed::Box;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use core::hash::{Hash, Hasher};
|
||||
use cranelift_codegen::ir;
|
||||
use cranelift_entity::{EntityRef, PrimaryMap};
|
||||
use cranelift_wasm::{
|
||||
DefinedFuncIndex, DefinedGlobalIndex, DefinedMemoryIndex, DefinedTableIndex, FuncIndex, Global,
|
||||
GlobalIndex, Memory, MemoryIndex, SignatureIndex, Table, TableIndex,
|
||||
};
|
||||
use indexmap::IndexMap;
|
||||
|
||||
/// A WebAssembly table initializer.
|
||||
#[derive(Clone, Debug, Hash)]
|
||||
pub struct TableElements {
|
||||
/// The index of a table to initialize.
|
||||
pub table_index: TableIndex,
|
||||
/// Optionally, a global variable giving a base index.
|
||||
pub base: Option<GlobalIndex>,
|
||||
/// The offset to add to the base.
|
||||
pub offset: usize,
|
||||
/// The values to write into the table elements.
|
||||
pub elements: Box<[FuncIndex]>,
|
||||
}
|
||||
|
||||
/// An entity to export.
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum Export {
|
||||
/// Function export.
|
||||
Function(FuncIndex),
|
||||
/// Table export.
|
||||
Table(TableIndex),
|
||||
/// Memory export.
|
||||
Memory(MemoryIndex),
|
||||
/// Global export.
|
||||
Global(GlobalIndex),
|
||||
}
|
||||
|
||||
/// Implemenation styles for WebAssembly linear memory.
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
pub enum MemoryStyle {
|
||||
/// The actual memory can be resized and moved.
|
||||
Dynamic,
|
||||
/// Addresss space is allocated up front.
|
||||
Static {
|
||||
/// The number of mapped and unmapped pages.
|
||||
bound: u32,
|
||||
},
|
||||
}
|
||||
|
||||
impl MemoryStyle {
|
||||
/// Decide on an implementation style for the given `Memory`.
|
||||
pub fn for_memory(memory: Memory, tunables: &Tunables) -> (Self, u64) {
|
||||
if let Some(maximum) = memory.maximum {
|
||||
if maximum <= tunables.static_memory_bound {
|
||||
// A heap with a declared maximum can be immovable, so make
|
||||
// it static.
|
||||
assert!(tunables.static_memory_bound >= memory.minimum);
|
||||
return (
|
||||
Self::Static {
|
||||
bound: tunables.static_memory_bound,
|
||||
},
|
||||
tunables.static_memory_offset_guard_size,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, make it dynamic.
|
||||
(Self::Dynamic, tunables.dynamic_memory_offset_guard_size)
|
||||
}
|
||||
}
|
||||
|
||||
/// A WebAssembly linear memory description along with our chosen style for
|
||||
/// implementing it.
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
pub struct MemoryPlan {
|
||||
/// The WebAssembly linear memory description.
|
||||
pub memory: Memory,
|
||||
/// Our chosen implementation style.
|
||||
pub style: MemoryStyle,
|
||||
/// Our chosen offset-guard size.
|
||||
pub offset_guard_size: u64,
|
||||
}
|
||||
|
||||
impl MemoryPlan {
|
||||
/// Draw up a plan for implementing a `Memory`.
|
||||
pub fn for_memory(memory: Memory, tunables: &Tunables) -> Self {
|
||||
let (style, offset_guard_size) = MemoryStyle::for_memory(memory, tunables);
|
||||
Self {
|
||||
memory,
|
||||
style,
|
||||
offset_guard_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implemenation styles for WebAssembly tables.
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
pub enum TableStyle {
|
||||
/// Signatures are stored in the table and checked in the caller.
|
||||
CallerChecksSignature,
|
||||
}
|
||||
|
||||
impl TableStyle {
|
||||
/// Decide on an implementation style for the given `Table`.
|
||||
pub fn for_table(_table: Table, _tunables: &Tunables) -> Self {
|
||||
Self::CallerChecksSignature
|
||||
}
|
||||
}
|
||||
|
||||
/// A WebAssembly table description along with our chosen style for
|
||||
/// implementing it.
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
pub struct TablePlan {
|
||||
/// The WebAssembly table description.
|
||||
pub table: cranelift_wasm::Table,
|
||||
/// Our chosen implementation style.
|
||||
pub style: TableStyle,
|
||||
}
|
||||
|
||||
impl TablePlan {
|
||||
/// Draw up a plan for implementing a `Table`.
|
||||
pub fn for_table(table: Table, tunables: &Tunables) -> Self {
|
||||
let style = TableStyle::for_table(table, tunables);
|
||||
Self { table, style }
|
||||
}
|
||||
}
|
||||
|
||||
/// A translated WebAssembly module, excluding the function bodies and
|
||||
/// memory initializers.
|
||||
// WARNING: when modifying, make sure that `hash_for_cache` is still valid!
|
||||
#[derive(Debug)]
|
||||
pub struct Module {
|
||||
/// Unprocessed signatures exactly as provided by `declare_signature()`.
|
||||
pub signatures: PrimaryMap<SignatureIndex, ir::Signature>,
|
||||
|
||||
/// Names of imported functions.
|
||||
pub imported_funcs: PrimaryMap<FuncIndex, (String, String)>,
|
||||
|
||||
/// Names of imported tables.
|
||||
pub imported_tables: PrimaryMap<TableIndex, (String, String)>,
|
||||
|
||||
/// Names of imported memories.
|
||||
pub imported_memories: PrimaryMap<MemoryIndex, (String, String)>,
|
||||
|
||||
/// Names of imported globals.
|
||||
pub imported_globals: PrimaryMap<GlobalIndex, (String, String)>,
|
||||
|
||||
/// Types of functions, imported and local.
|
||||
pub functions: PrimaryMap<FuncIndex, SignatureIndex>,
|
||||
|
||||
/// WebAssembly tables.
|
||||
pub table_plans: PrimaryMap<TableIndex, TablePlan>,
|
||||
|
||||
/// WebAssembly linear memory plans.
|
||||
pub memory_plans: PrimaryMap<MemoryIndex, MemoryPlan>,
|
||||
|
||||
/// WebAssembly global variables.
|
||||
pub globals: PrimaryMap<GlobalIndex, Global>,
|
||||
|
||||
/// Exported entities.
|
||||
pub exports: IndexMap<String, Export>,
|
||||
|
||||
/// The module "start" function, if present.
|
||||
pub start_func: Option<FuncIndex>,
|
||||
|
||||
/// WebAssembly table initializers.
|
||||
pub table_elements: Vec<TableElements>,
|
||||
}
|
||||
|
||||
impl Module {
|
||||
/// Allocates the module data structures.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
signatures: PrimaryMap::new(),
|
||||
imported_funcs: PrimaryMap::new(),
|
||||
imported_tables: PrimaryMap::new(),
|
||||
imported_memories: PrimaryMap::new(),
|
||||
imported_globals: PrimaryMap::new(),
|
||||
functions: PrimaryMap::new(),
|
||||
table_plans: PrimaryMap::new(),
|
||||
memory_plans: PrimaryMap::new(),
|
||||
globals: PrimaryMap::new(),
|
||||
exports: IndexMap::new(),
|
||||
start_func: None,
|
||||
table_elements: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a `DefinedFuncIndex` into a `FuncIndex`.
|
||||
pub fn func_index(&self, defined_func: DefinedFuncIndex) -> FuncIndex {
|
||||
FuncIndex::new(self.imported_funcs.len() + defined_func.index())
|
||||
}
|
||||
|
||||
/// Convert a `FuncIndex` into a `DefinedFuncIndex`. Returns None if the
|
||||
/// index is an imported function.
|
||||
pub fn defined_func_index(&self, func: FuncIndex) -> Option<DefinedFuncIndex> {
|
||||
if func.index() < self.imported_funcs.len() {
|
||||
None
|
||||
} else {
|
||||
Some(DefinedFuncIndex::new(
|
||||
func.index() - self.imported_funcs.len(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Test whether the given function index is for an imported function.
|
||||
pub fn is_imported_function(&self, index: FuncIndex) -> bool {
|
||||
index.index() < self.imported_funcs.len()
|
||||
}
|
||||
|
||||
/// Convert a `DefinedTableIndex` into a `TableIndex`.
|
||||
pub fn table_index(&self, defined_table: DefinedTableIndex) -> TableIndex {
|
||||
TableIndex::new(self.imported_tables.len() + defined_table.index())
|
||||
}
|
||||
|
||||
/// Convert a `TableIndex` into a `DefinedTableIndex`. Returns None if the
|
||||
/// index is an imported table.
|
||||
pub fn defined_table_index(&self, table: TableIndex) -> Option<DefinedTableIndex> {
|
||||
if table.index() < self.imported_tables.len() {
|
||||
None
|
||||
} else {
|
||||
Some(DefinedTableIndex::new(
|
||||
table.index() - self.imported_tables.len(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Test whether the given table index is for an imported table.
|
||||
pub fn is_imported_table(&self, index: TableIndex) -> bool {
|
||||
index.index() < self.imported_tables.len()
|
||||
}
|
||||
|
||||
/// Convert a `DefinedMemoryIndex` into a `MemoryIndex`.
|
||||
pub fn memory_index(&self, defined_memory: DefinedMemoryIndex) -> MemoryIndex {
|
||||
MemoryIndex::new(self.imported_memories.len() + defined_memory.index())
|
||||
}
|
||||
|
||||
/// Convert a `MemoryIndex` into a `DefinedMemoryIndex`. Returns None if the
|
||||
/// index is an imported memory.
|
||||
pub fn defined_memory_index(&self, memory: MemoryIndex) -> Option<DefinedMemoryIndex> {
|
||||
if memory.index() < self.imported_memories.len() {
|
||||
None
|
||||
} else {
|
||||
Some(DefinedMemoryIndex::new(
|
||||
memory.index() - self.imported_memories.len(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Test whether the given memory index is for an imported memory.
|
||||
pub fn is_imported_memory(&self, index: MemoryIndex) -> bool {
|
||||
index.index() < self.imported_memories.len()
|
||||
}
|
||||
|
||||
/// Convert a `DefinedGlobalIndex` into a `GlobalIndex`.
|
||||
pub fn global_index(&self, defined_global: DefinedGlobalIndex) -> GlobalIndex {
|
||||
GlobalIndex::new(self.imported_globals.len() + defined_global.index())
|
||||
}
|
||||
|
||||
/// Convert a `GlobalIndex` into a `DefinedGlobalIndex`. Returns None if the
|
||||
/// index is an imported global.
|
||||
pub fn defined_global_index(&self, global: GlobalIndex) -> Option<DefinedGlobalIndex> {
|
||||
if global.index() < self.imported_globals.len() {
|
||||
None
|
||||
} else {
|
||||
Some(DefinedGlobalIndex::new(
|
||||
global.index() - self.imported_globals.len(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Test whether the given global index is for an imported global.
|
||||
pub fn is_imported_global(&self, index: GlobalIndex) -> bool {
|
||||
index.index() < self.imported_globals.len()
|
||||
}
|
||||
|
||||
/// Computes hash of the module for the purpose of caching.
|
||||
pub fn hash_for_cache<'data, H>(
|
||||
&self,
|
||||
function_body_inputs: &PrimaryMap<DefinedFuncIndex, FunctionBodyData<'data>>,
|
||||
state: &mut H,
|
||||
) where
|
||||
H: Hasher,
|
||||
{
|
||||
// There's no need to cache names (strings), start function
|
||||
// and data initializers (for both memory and tables)
|
||||
self.signatures.hash(state);
|
||||
self.functions.hash(state);
|
||||
self.table_plans.hash(state);
|
||||
self.memory_plans.hash(state);
|
||||
self.globals.hash(state);
|
||||
// IndexMap (self.export) iterates over values in order of item inserts
|
||||
// Let's actually sort the values.
|
||||
let mut exports = self.exports.values().collect::<Vec<_>>();
|
||||
exports.sort();
|
||||
for val in exports {
|
||||
val.hash(state);
|
||||
}
|
||||
function_body_inputs.hash(state);
|
||||
}
|
||||
}
|
||||
396
crates/environ/src/module_environ.rs
Normal file
396
crates/environ/src/module_environ.rs
Normal file
@@ -0,0 +1,396 @@
|
||||
use crate::func_environ::FuncEnvironment;
|
||||
use crate::module::{Export, MemoryPlan, Module, TableElements, TablePlan};
|
||||
use crate::tunables::Tunables;
|
||||
use alloc::boxed::Box;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use core::convert::TryFrom;
|
||||
use cranelift_codegen::ir;
|
||||
use cranelift_codegen::ir::{AbiParam, ArgumentPurpose};
|
||||
use cranelift_codegen::isa::TargetFrontendConfig;
|
||||
use cranelift_entity::PrimaryMap;
|
||||
use cranelift_wasm::{
|
||||
self, translate_module, DefinedFuncIndex, FuncIndex, Global, GlobalIndex, Memory, MemoryIndex,
|
||||
ModuleTranslationState, SignatureIndex, Table, TableIndex, WasmResult,
|
||||
};
|
||||
|
||||
/// Contains function data: byte code and its offset in the module.
|
||||
#[derive(Hash)]
|
||||
pub struct FunctionBodyData<'a> {
|
||||
/// Body byte code.
|
||||
pub data: &'a [u8],
|
||||
|
||||
/// Body offset in the module file.
|
||||
pub module_offset: usize,
|
||||
}
|
||||
|
||||
/// The result of translating via `ModuleEnvironment`. Function bodies are not
|
||||
/// yet translated, and data initializers have not yet been copied out of the
|
||||
/// original buffer.
|
||||
pub struct ModuleTranslation<'data> {
|
||||
/// Compilation setting flags.
|
||||
pub target_config: TargetFrontendConfig,
|
||||
|
||||
/// Module information.
|
||||
pub module: Module,
|
||||
|
||||
/// References to the function bodies.
|
||||
pub function_body_inputs: PrimaryMap<DefinedFuncIndex, FunctionBodyData<'data>>,
|
||||
|
||||
/// References to the data initializers.
|
||||
pub data_initializers: Vec<DataInitializer<'data>>,
|
||||
|
||||
/// Tunable parameters.
|
||||
pub tunables: Tunables,
|
||||
|
||||
/// The decoded Wasm types for the module.
|
||||
pub module_translation: Option<ModuleTranslationState>,
|
||||
}
|
||||
|
||||
impl<'data> ModuleTranslation<'data> {
|
||||
/// Return a new `FuncEnvironment` for translating a function.
|
||||
pub fn func_env(&self) -> FuncEnvironment<'_> {
|
||||
FuncEnvironment::new(self.target_config, &self.module)
|
||||
}
|
||||
}
|
||||
|
||||
/// Object containing the standalone environment information.
|
||||
pub struct ModuleEnvironment<'data> {
|
||||
/// The result to be filled in.
|
||||
result: ModuleTranslation<'data>,
|
||||
}
|
||||
|
||||
impl<'data> ModuleEnvironment<'data> {
|
||||
/// Allocates the enironment data structures.
|
||||
pub fn new(target_config: TargetFrontendConfig, tunables: Tunables) -> Self {
|
||||
Self {
|
||||
result: ModuleTranslation {
|
||||
target_config,
|
||||
module: Module::new(),
|
||||
function_body_inputs: PrimaryMap::new(),
|
||||
data_initializers: Vec::new(),
|
||||
tunables,
|
||||
module_translation: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn pointer_type(&self) -> ir::Type {
|
||||
self.result.target_config.pointer_type()
|
||||
}
|
||||
|
||||
/// Translate a wasm module using this environment. This consumes the
|
||||
/// `ModuleEnvironment` and produces a `ModuleTranslation`.
|
||||
pub fn translate(mut self, data: &'data [u8]) -> WasmResult<ModuleTranslation<'data>> {
|
||||
assert!(self.result.module_translation.is_none());
|
||||
let module_translation = translate_module(data, &mut self)?;
|
||||
self.result.module_translation = Some(module_translation);
|
||||
Ok(self.result)
|
||||
}
|
||||
}
|
||||
|
||||
/// This trait is useful for `translate_module` because it tells how to translate
|
||||
/// enironment-dependent wasm instructions. These functions should not be called by the user.
|
||||
impl<'data> cranelift_wasm::ModuleEnvironment<'data> for ModuleEnvironment<'data> {
|
||||
fn target_config(&self) -> TargetFrontendConfig {
|
||||
self.result.target_config
|
||||
}
|
||||
|
||||
fn reserve_signatures(&mut self, num: u32) -> WasmResult<()> {
|
||||
self.result
|
||||
.module
|
||||
.signatures
|
||||
.reserve_exact(usize::try_from(num).unwrap());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_signature(&mut self, sig: ir::Signature) -> WasmResult<()> {
|
||||
let sig = translate_signature(sig, self.pointer_type());
|
||||
// TODO: Deduplicate signatures.
|
||||
self.result.module.signatures.push(sig);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_func_import(
|
||||
&mut self,
|
||||
sig_index: SignatureIndex,
|
||||
module: &str,
|
||||
field: &str,
|
||||
) -> WasmResult<()> {
|
||||
debug_assert_eq!(
|
||||
self.result.module.functions.len(),
|
||||
self.result.module.imported_funcs.len(),
|
||||
"Imported functions must be declared first"
|
||||
);
|
||||
self.result.module.functions.push(sig_index);
|
||||
|
||||
self.result
|
||||
.module
|
||||
.imported_funcs
|
||||
.push((String::from(module), String::from(field)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_table_import(&mut self, table: Table, module: &str, field: &str) -> WasmResult<()> {
|
||||
debug_assert_eq!(
|
||||
self.result.module.table_plans.len(),
|
||||
self.result.module.imported_tables.len(),
|
||||
"Imported tables must be declared first"
|
||||
);
|
||||
let plan = TablePlan::for_table(table, &self.result.tunables);
|
||||
self.result.module.table_plans.push(plan);
|
||||
|
||||
self.result
|
||||
.module
|
||||
.imported_tables
|
||||
.push((String::from(module), String::from(field)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_memory_import(
|
||||
&mut self,
|
||||
memory: Memory,
|
||||
module: &str,
|
||||
field: &str,
|
||||
) -> WasmResult<()> {
|
||||
debug_assert_eq!(
|
||||
self.result.module.memory_plans.len(),
|
||||
self.result.module.imported_memories.len(),
|
||||
"Imported memories must be declared first"
|
||||
);
|
||||
let plan = MemoryPlan::for_memory(memory, &self.result.tunables);
|
||||
self.result.module.memory_plans.push(plan);
|
||||
|
||||
self.result
|
||||
.module
|
||||
.imported_memories
|
||||
.push((String::from(module), String::from(field)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_global_import(
|
||||
&mut self,
|
||||
global: Global,
|
||||
module: &str,
|
||||
field: &str,
|
||||
) -> WasmResult<()> {
|
||||
debug_assert_eq!(
|
||||
self.result.module.globals.len(),
|
||||
self.result.module.imported_globals.len(),
|
||||
"Imported globals must be declared first"
|
||||
);
|
||||
self.result.module.globals.push(global);
|
||||
|
||||
self.result
|
||||
.module
|
||||
.imported_globals
|
||||
.push((String::from(module), String::from(field)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn finish_imports(&mut self) -> WasmResult<()> {
|
||||
self.result.module.imported_funcs.shrink_to_fit();
|
||||
self.result.module.imported_tables.shrink_to_fit();
|
||||
self.result.module.imported_memories.shrink_to_fit();
|
||||
self.result.module.imported_globals.shrink_to_fit();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reserve_func_types(&mut self, num: u32) -> WasmResult<()> {
|
||||
self.result
|
||||
.module
|
||||
.functions
|
||||
.reserve_exact(usize::try_from(num).unwrap());
|
||||
self.result
|
||||
.function_body_inputs
|
||||
.reserve_exact(usize::try_from(num).unwrap());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_func_type(&mut self, sig_index: SignatureIndex) -> WasmResult<()> {
|
||||
self.result.module.functions.push(sig_index);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reserve_tables(&mut self, num: u32) -> WasmResult<()> {
|
||||
self.result
|
||||
.module
|
||||
.table_plans
|
||||
.reserve_exact(usize::try_from(num).unwrap());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_table(&mut self, table: Table) -> WasmResult<()> {
|
||||
let plan = TablePlan::for_table(table, &self.result.tunables);
|
||||
self.result.module.table_plans.push(plan);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reserve_memories(&mut self, num: u32) -> WasmResult<()> {
|
||||
self.result
|
||||
.module
|
||||
.memory_plans
|
||||
.reserve_exact(usize::try_from(num).unwrap());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_memory(&mut self, memory: Memory) -> WasmResult<()> {
|
||||
let plan = MemoryPlan::for_memory(memory, &self.result.tunables);
|
||||
self.result.module.memory_plans.push(plan);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reserve_globals(&mut self, num: u32) -> WasmResult<()> {
|
||||
self.result
|
||||
.module
|
||||
.globals
|
||||
.reserve_exact(usize::try_from(num).unwrap());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_global(&mut self, global: Global) -> WasmResult<()> {
|
||||
self.result.module.globals.push(global);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reserve_exports(&mut self, num: u32) -> WasmResult<()> {
|
||||
self.result
|
||||
.module
|
||||
.exports
|
||||
.reserve(usize::try_from(num).unwrap());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_func_export(&mut self, func_index: FuncIndex, name: &str) -> WasmResult<()> {
|
||||
self.result
|
||||
.module
|
||||
.exports
|
||||
.insert(String::from(name), Export::Function(func_index));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_table_export(&mut self, table_index: TableIndex, name: &str) -> WasmResult<()> {
|
||||
self.result
|
||||
.module
|
||||
.exports
|
||||
.insert(String::from(name), Export::Table(table_index));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_memory_export(&mut self, memory_index: MemoryIndex, name: &str) -> WasmResult<()> {
|
||||
self.result
|
||||
.module
|
||||
.exports
|
||||
.insert(String::from(name), Export::Memory(memory_index));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_global_export(&mut self, global_index: GlobalIndex, name: &str) -> WasmResult<()> {
|
||||
self.result
|
||||
.module
|
||||
.exports
|
||||
.insert(String::from(name), Export::Global(global_index));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_start_func(&mut self, func_index: FuncIndex) -> WasmResult<()> {
|
||||
debug_assert!(self.result.module.start_func.is_none());
|
||||
self.result.module.start_func = Some(func_index);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reserve_table_elements(&mut self, num: u32) -> WasmResult<()> {
|
||||
self.result
|
||||
.module
|
||||
.table_elements
|
||||
.reserve_exact(usize::try_from(num).unwrap());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_table_elements(
|
||||
&mut self,
|
||||
table_index: TableIndex,
|
||||
base: Option<GlobalIndex>,
|
||||
offset: usize,
|
||||
elements: Box<[FuncIndex]>,
|
||||
) -> WasmResult<()> {
|
||||
self.result.module.table_elements.push(TableElements {
|
||||
table_index,
|
||||
base,
|
||||
offset,
|
||||
elements,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn define_function_body(
|
||||
&mut self,
|
||||
_module_translation: &ModuleTranslationState,
|
||||
body_bytes: &'data [u8],
|
||||
body_offset: usize,
|
||||
) -> WasmResult<()> {
|
||||
self.result.function_body_inputs.push(FunctionBodyData {
|
||||
data: body_bytes,
|
||||
module_offset: body_offset,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reserve_data_initializers(&mut self, num: u32) -> WasmResult<()> {
|
||||
self.result
|
||||
.data_initializers
|
||||
.reserve_exact(usize::try_from(num).unwrap());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn declare_data_initialization(
|
||||
&mut self,
|
||||
memory_index: MemoryIndex,
|
||||
base: Option<GlobalIndex>,
|
||||
offset: usize,
|
||||
data: &'data [u8],
|
||||
) -> WasmResult<()> {
|
||||
self.result.data_initializers.push(DataInitializer {
|
||||
location: DataInitializerLocation {
|
||||
memory_index,
|
||||
base,
|
||||
offset,
|
||||
},
|
||||
data,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Add environment-specific function parameters.
|
||||
pub fn translate_signature(mut sig: ir::Signature, pointer_type: ir::Type) -> ir::Signature {
|
||||
// Prepend the vmctx argument.
|
||||
sig.params.insert(
|
||||
0,
|
||||
AbiParam::special(pointer_type, ArgumentPurpose::VMContext),
|
||||
);
|
||||
sig
|
||||
}
|
||||
|
||||
/// A memory index and offset within that memory where a data initialization
|
||||
/// should is to be performed.
|
||||
#[derive(Clone)]
|
||||
pub struct DataInitializerLocation {
|
||||
/// The index of the memory to initialize.
|
||||
pub memory_index: MemoryIndex,
|
||||
|
||||
/// Optionally a globalvar base to initialize at.
|
||||
pub base: Option<GlobalIndex>,
|
||||
|
||||
/// A constant offset to initialize at.
|
||||
pub offset: usize,
|
||||
}
|
||||
|
||||
/// A data initializer for linear memory.
|
||||
pub struct DataInitializer<'data> {
|
||||
/// The location where the initialization is to be performed.
|
||||
pub location: DataInitializerLocation,
|
||||
|
||||
/// The initialization data.
|
||||
pub data: &'data [u8],
|
||||
}
|
||||
44
crates/environ/src/tunables.rs
Normal file
44
crates/environ/src/tunables.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
/// Tunable parameters for WebAssembly compilation.
|
||||
#[derive(Clone)]
|
||||
pub struct Tunables {
|
||||
/// For static heaps, the size of the heap protected by bounds checking.
|
||||
pub static_memory_bound: u32,
|
||||
|
||||
/// The size of the offset guard for static heaps.
|
||||
pub static_memory_offset_guard_size: u64,
|
||||
|
||||
/// The size of the offset guard for dynamic heaps.
|
||||
pub dynamic_memory_offset_guard_size: u64,
|
||||
}
|
||||
|
||||
impl Default for Tunables {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
/// Size in wasm pages of the bound for static memories.
|
||||
static_memory_bound: 0x4000,
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
/// Size in wasm pages of the bound for static memories.
|
||||
///
|
||||
/// When we allocate 4 GiB of address space, we can avoid the
|
||||
/// need for explicit bounds checks.
|
||||
static_memory_bound: 0x1_0000,
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
/// Size in bytes of the offset guard for static memories.
|
||||
static_memory_offset_guard_size: 0x1_0000,
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
/// Size in bytes of the offset guard for static memories.
|
||||
///
|
||||
/// Allocating 2 GiB of address space lets us translate wasm
|
||||
/// offsets into x86 offsets as aggressively as we can.
|
||||
static_memory_offset_guard_size: 0x8000_0000,
|
||||
|
||||
/// Size in bytes of the offset guard for dynamic memories.
|
||||
///
|
||||
/// Allocate a small guard to optimize common cases but without
|
||||
/// wasting too much memor.
|
||||
dynamic_memory_offset_guard_size: 0x1_0000,
|
||||
}
|
||||
}
|
||||
}
|
||||
583
crates/environ/src/vmoffsets.rs
Normal file
583
crates/environ/src/vmoffsets.rs
Normal file
@@ -0,0 +1,583 @@
|
||||
//! Offsets and sizes of various structs in wasmtime-runtime's vmcontext
|
||||
//! module.
|
||||
|
||||
use crate::module::Module;
|
||||
use crate::BuiltinFunctionIndex;
|
||||
use core::convert::TryFrom;
|
||||
use cranelift_codegen::ir;
|
||||
use cranelift_wasm::{
|
||||
DefinedGlobalIndex, DefinedMemoryIndex, DefinedTableIndex, FuncIndex, GlobalIndex, MemoryIndex,
|
||||
SignatureIndex, TableIndex,
|
||||
};
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
fn cast_to_u32(sz: usize) -> u32 {
|
||||
u32::try_from(sz).unwrap()
|
||||
}
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
fn cast_to_u32(sz: usize) -> u32 {
|
||||
match u32::try_from(sz) {
|
||||
Ok(x) => x,
|
||||
Err(_) => panic!("overflow in cast from usize to u32"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Align an offset used in this module to a specific byte-width by rounding up
|
||||
fn align(offset: u32, width: u32) -> u32 {
|
||||
(offset + (width - 1)) / width * width
|
||||
}
|
||||
|
||||
/// This class computes offsets to fields within `VMContext` and other
|
||||
/// related structs that JIT code accesses directly.
|
||||
pub struct VMOffsets {
|
||||
/// The size in bytes of a pointer on the target.
|
||||
pub pointer_size: u8,
|
||||
/// The number of signature declarations in the module.
|
||||
pub num_signature_ids: u32,
|
||||
/// The number of imported functions in the module.
|
||||
pub num_imported_functions: u32,
|
||||
/// The number of imported tables in the module.
|
||||
pub num_imported_tables: u32,
|
||||
/// The number of imported memories in the module.
|
||||
pub num_imported_memories: u32,
|
||||
/// The number of imported globals in the module.
|
||||
pub num_imported_globals: u32,
|
||||
/// The number of defined tables in the module.
|
||||
pub num_defined_tables: u32,
|
||||
/// The number of defined memories in the module.
|
||||
pub num_defined_memories: u32,
|
||||
/// The number of defined globals in the module.
|
||||
pub num_defined_globals: u32,
|
||||
}
|
||||
|
||||
impl VMOffsets {
|
||||
/// Return a new `VMOffsets` instance, for a given pointer size.
|
||||
pub fn new(pointer_size: u8, module: &Module) -> Self {
|
||||
Self {
|
||||
pointer_size,
|
||||
num_signature_ids: cast_to_u32(module.signatures.len()),
|
||||
num_imported_functions: cast_to_u32(module.imported_funcs.len()),
|
||||
num_imported_tables: cast_to_u32(module.imported_tables.len()),
|
||||
num_imported_memories: cast_to_u32(module.imported_memories.len()),
|
||||
num_imported_globals: cast_to_u32(module.imported_globals.len()),
|
||||
num_defined_tables: cast_to_u32(module.table_plans.len()),
|
||||
num_defined_memories: cast_to_u32(module.memory_plans.len()),
|
||||
num_defined_globals: cast_to_u32(module.globals.len()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Offsets for `VMFunctionImport`.
|
||||
impl VMOffsets {
|
||||
/// The offset of the `body` field.
|
||||
#[allow(clippy::erasing_op)]
|
||||
pub fn vmfunction_import_body(&self) -> u8 {
|
||||
0 * self.pointer_size
|
||||
}
|
||||
|
||||
/// The offset of the `vmctx` field.
|
||||
#[allow(clippy::identity_op)]
|
||||
pub fn vmfunction_import_vmctx(&self) -> u8 {
|
||||
1 * self.pointer_size
|
||||
}
|
||||
|
||||
/// Return the size of `VMFunctionImport`.
|
||||
pub fn size_of_vmfunction_import(&self) -> u8 {
|
||||
2 * self.pointer_size
|
||||
}
|
||||
}
|
||||
|
||||
/// Offsets for `*const VMFunctionBody`.
|
||||
impl VMOffsets {
|
||||
/// The size of the `current_elements` field.
|
||||
#[allow(clippy::identity_op)]
|
||||
pub fn size_of_vmfunction_body_ptr(&self) -> u8 {
|
||||
1 * self.pointer_size
|
||||
}
|
||||
}
|
||||
|
||||
/// Offsets for `VMTableImport`.
|
||||
impl VMOffsets {
|
||||
/// The offset of the `from` field.
|
||||
#[allow(clippy::erasing_op)]
|
||||
pub fn vmtable_import_from(&self) -> u8 {
|
||||
0 * self.pointer_size
|
||||
}
|
||||
|
||||
/// The offset of the `vmctx` field.
|
||||
#[allow(clippy::identity_op)]
|
||||
pub fn vmtable_import_vmctx(&self) -> u8 {
|
||||
1 * self.pointer_size
|
||||
}
|
||||
|
||||
/// Return the size of `VMTableImport`.
|
||||
pub fn size_of_vmtable_import(&self) -> u8 {
|
||||
2 * self.pointer_size
|
||||
}
|
||||
}
|
||||
|
||||
/// Offsets for `VMTableDefinition`.
|
||||
impl VMOffsets {
|
||||
/// The offset of the `base` field.
|
||||
#[allow(clippy::erasing_op)]
|
||||
pub fn vmtable_definition_base(&self) -> u8 {
|
||||
0 * self.pointer_size
|
||||
}
|
||||
|
||||
/// The offset of the `current_elements` field.
|
||||
#[allow(clippy::identity_op)]
|
||||
pub fn vmtable_definition_current_elements(&self) -> u8 {
|
||||
1 * self.pointer_size
|
||||
}
|
||||
|
||||
/// The size of the `current_elements` field.
|
||||
pub fn size_of_vmtable_definition_current_elements(&self) -> u8 {
|
||||
4
|
||||
}
|
||||
|
||||
/// Return the size of `VMTableDefinition`.
|
||||
pub fn size_of_vmtable_definition(&self) -> u8 {
|
||||
2 * self.pointer_size
|
||||
}
|
||||
|
||||
/// The type of the `current_elements` field.
|
||||
pub fn type_of_vmtable_definition_current_elements(&self) -> ir::Type {
|
||||
ir::Type::int(u16::from(self.size_of_vmtable_definition_current_elements()) * 8).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Offsets for `VMMemoryImport`.
|
||||
impl VMOffsets {
|
||||
/// The offset of the `from` field.
|
||||
#[allow(clippy::erasing_op)]
|
||||
pub fn vmmemory_import_from(&self) -> u8 {
|
||||
0 * self.pointer_size
|
||||
}
|
||||
|
||||
/// The offset of the `vmctx` field.
|
||||
#[allow(clippy::identity_op)]
|
||||
pub fn vmmemory_import_vmctx(&self) -> u8 {
|
||||
1 * self.pointer_size
|
||||
}
|
||||
|
||||
/// Return the size of `VMMemoryImport`.
|
||||
pub fn size_of_vmmemory_import(&self) -> u8 {
|
||||
2 * self.pointer_size
|
||||
}
|
||||
}
|
||||
|
||||
/// Offsets for `VMMemoryDefinition`.
|
||||
impl VMOffsets {
|
||||
/// The offset of the `base` field.
|
||||
#[allow(clippy::erasing_op)]
|
||||
pub fn vmmemory_definition_base(&self) -> u8 {
|
||||
0 * self.pointer_size
|
||||
}
|
||||
|
||||
/// The offset of the `current_length` field.
|
||||
#[allow(clippy::identity_op)]
|
||||
pub fn vmmemory_definition_current_length(&self) -> u8 {
|
||||
1 * self.pointer_size
|
||||
}
|
||||
|
||||
/// The size of the `current_length` field.
|
||||
pub fn size_of_vmmemory_definition_current_length(&self) -> u8 {
|
||||
4
|
||||
}
|
||||
|
||||
/// Return the size of `VMMemoryDefinition`.
|
||||
pub fn size_of_vmmemory_definition(&self) -> u8 {
|
||||
2 * self.pointer_size
|
||||
}
|
||||
|
||||
/// The type of the `current_length` field.
|
||||
pub fn type_of_vmmemory_definition_current_length(&self) -> ir::Type {
|
||||
ir::Type::int(u16::from(self.size_of_vmmemory_definition_current_length()) * 8).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Offsets for `VMGlobalImport`.
|
||||
impl VMOffsets {
|
||||
/// The offset of the `from` field.
|
||||
#[allow(clippy::erasing_op)]
|
||||
pub fn vmglobal_import_from(&self) -> u8 {
|
||||
0 * self.pointer_size
|
||||
}
|
||||
|
||||
/// Return the size of `VMGlobalImport`.
|
||||
#[allow(clippy::identity_op)]
|
||||
pub fn size_of_vmglobal_import(&self) -> u8 {
|
||||
1 * self.pointer_size
|
||||
}
|
||||
}
|
||||
|
||||
/// Offsets for `VMGlobalDefinition`.
|
||||
impl VMOffsets {
|
||||
/// Return the size of `VMGlobalDefinition`; this is the size of the largest value type (i.e. a
|
||||
/// V128).
|
||||
pub fn size_of_vmglobal_definition(&self) -> u8 {
|
||||
16
|
||||
}
|
||||
}
|
||||
|
||||
/// Offsets for `VMSharedSignatureIndex`.
|
||||
impl VMOffsets {
|
||||
/// Return the size of `VMSharedSignatureIndex`.
|
||||
pub fn size_of_vmshared_signature_index(&self) -> u8 {
|
||||
4
|
||||
}
|
||||
}
|
||||
|
||||
/// Offsets for `VMCallerCheckedAnyfunc`.
|
||||
impl VMOffsets {
|
||||
/// The offset of the `func_ptr` field.
|
||||
#[allow(clippy::erasing_op)]
|
||||
pub fn vmcaller_checked_anyfunc_func_ptr(&self) -> u8 {
|
||||
0 * self.pointer_size
|
||||
}
|
||||
|
||||
/// The offset of the `type_index` field.
|
||||
#[allow(clippy::identity_op)]
|
||||
pub fn vmcaller_checked_anyfunc_type_index(&self) -> u8 {
|
||||
1 * self.pointer_size
|
||||
}
|
||||
|
||||
/// The offset of the `vmctx` field.
|
||||
pub fn vmcaller_checked_anyfunc_vmctx(&self) -> u8 {
|
||||
2 * self.pointer_size
|
||||
}
|
||||
|
||||
/// Return the size of `VMCallerCheckedAnyfunc`.
|
||||
pub fn size_of_vmcaller_checked_anyfunc(&self) -> u8 {
|
||||
3 * self.pointer_size
|
||||
}
|
||||
}
|
||||
|
||||
/// Offsets for `VMContext`.
|
||||
impl VMOffsets {
|
||||
/// The offset of the `signature_ids` array.
|
||||
pub fn vmctx_signature_ids_begin(&self) -> u32 {
|
||||
0
|
||||
}
|
||||
|
||||
/// The offset of the `tables` array.
|
||||
#[allow(clippy::erasing_op)]
|
||||
pub fn vmctx_imported_functions_begin(&self) -> u32 {
|
||||
self.vmctx_signature_ids_begin()
|
||||
.checked_add(
|
||||
self.num_signature_ids
|
||||
.checked_mul(u32::from(self.size_of_vmshared_signature_index()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The offset of the `tables` array.
|
||||
#[allow(clippy::identity_op)]
|
||||
pub fn vmctx_imported_tables_begin(&self) -> u32 {
|
||||
self.vmctx_imported_functions_begin()
|
||||
.checked_add(
|
||||
self.num_imported_functions
|
||||
.checked_mul(u32::from(self.size_of_vmfunction_import()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The offset of the `memories` array.
|
||||
pub fn vmctx_imported_memories_begin(&self) -> u32 {
|
||||
self.vmctx_imported_tables_begin()
|
||||
.checked_add(
|
||||
self.num_imported_tables
|
||||
.checked_mul(u32::from(self.size_of_vmtable_import()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The offset of the `globals` array.
|
||||
pub fn vmctx_imported_globals_begin(&self) -> u32 {
|
||||
self.vmctx_imported_memories_begin()
|
||||
.checked_add(
|
||||
self.num_imported_memories
|
||||
.checked_mul(u32::from(self.size_of_vmmemory_import()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The offset of the `tables` array.
|
||||
pub fn vmctx_tables_begin(&self) -> u32 {
|
||||
self.vmctx_imported_globals_begin()
|
||||
.checked_add(
|
||||
self.num_imported_globals
|
||||
.checked_mul(u32::from(self.size_of_vmglobal_import()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The offset of the `memories` array.
|
||||
pub fn vmctx_memories_begin(&self) -> u32 {
|
||||
self.vmctx_tables_begin()
|
||||
.checked_add(
|
||||
self.num_defined_tables
|
||||
.checked_mul(u32::from(self.size_of_vmtable_definition()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The offset of the `globals` array.
|
||||
pub fn vmctx_globals_begin(&self) -> u32 {
|
||||
let offset = self
|
||||
.vmctx_memories_begin()
|
||||
.checked_add(
|
||||
self.num_defined_memories
|
||||
.checked_mul(u32::from(self.size_of_vmmemory_definition()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
align(offset, 16)
|
||||
}
|
||||
|
||||
/// The offset of the builtin functions array.
|
||||
pub fn vmctx_builtin_functions_begin(&self) -> u32 {
|
||||
self.vmctx_globals_begin()
|
||||
.checked_add(
|
||||
self.num_defined_globals
|
||||
.checked_mul(u32::from(self.size_of_vmglobal_definition()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the size of the `VMContext` allocation.
|
||||
pub fn size_of_vmctx(&self) -> u32 {
|
||||
self.vmctx_builtin_functions_begin()
|
||||
.checked_add(
|
||||
BuiltinFunctionIndex::builtin_functions_total_number()
|
||||
.checked_mul(u32::from(self.pointer_size))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to `VMSharedSignatureId` index `index`.
|
||||
pub fn vmctx_vmshared_signature_id(&self, index: SignatureIndex) -> u32 {
|
||||
assert!(index.as_u32() < self.num_signature_ids);
|
||||
self.vmctx_signature_ids_begin()
|
||||
.checked_add(
|
||||
index
|
||||
.as_u32()
|
||||
.checked_mul(u32::from(self.size_of_vmshared_signature_index()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to `VMFunctionImport` index `index`.
|
||||
pub fn vmctx_vmfunction_import(&self, index: FuncIndex) -> u32 {
|
||||
assert!(index.as_u32() < self.num_imported_functions);
|
||||
self.vmctx_imported_functions_begin()
|
||||
.checked_add(
|
||||
index
|
||||
.as_u32()
|
||||
.checked_mul(u32::from(self.size_of_vmfunction_import()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to `VMTableImport` index `index`.
|
||||
pub fn vmctx_vmtable_import(&self, index: TableIndex) -> u32 {
|
||||
assert!(index.as_u32() < self.num_imported_tables);
|
||||
self.vmctx_imported_tables_begin()
|
||||
.checked_add(
|
||||
index
|
||||
.as_u32()
|
||||
.checked_mul(u32::from(self.size_of_vmtable_import()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to `VMMemoryImport` index `index`.
|
||||
pub fn vmctx_vmmemory_import(&self, index: MemoryIndex) -> u32 {
|
||||
assert!(index.as_u32() < self.num_imported_memories);
|
||||
self.vmctx_imported_memories_begin()
|
||||
.checked_add(
|
||||
index
|
||||
.as_u32()
|
||||
.checked_mul(u32::from(self.size_of_vmmemory_import()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to `VMGlobalImport` index `index`.
|
||||
pub fn vmctx_vmglobal_import(&self, index: GlobalIndex) -> u32 {
|
||||
assert!(index.as_u32() < self.num_imported_globals);
|
||||
self.vmctx_imported_globals_begin()
|
||||
.checked_add(
|
||||
index
|
||||
.as_u32()
|
||||
.checked_mul(u32::from(self.size_of_vmglobal_import()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to `VMTableDefinition` index `index`.
|
||||
pub fn vmctx_vmtable_definition(&self, index: DefinedTableIndex) -> u32 {
|
||||
assert!(index.as_u32() < self.num_defined_tables);
|
||||
self.vmctx_tables_begin()
|
||||
.checked_add(
|
||||
index
|
||||
.as_u32()
|
||||
.checked_mul(u32::from(self.size_of_vmtable_definition()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to `VMMemoryDefinition` index `index`.
|
||||
pub fn vmctx_vmmemory_definition(&self, index: DefinedMemoryIndex) -> u32 {
|
||||
assert!(index.as_u32() < self.num_defined_memories);
|
||||
self.vmctx_memories_begin()
|
||||
.checked_add(
|
||||
index
|
||||
.as_u32()
|
||||
.checked_mul(u32::from(self.size_of_vmmemory_definition()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to the `VMGlobalDefinition` index `index`.
|
||||
pub fn vmctx_vmglobal_definition(&self, index: DefinedGlobalIndex) -> u32 {
|
||||
assert!(index.as_u32() < self.num_defined_globals);
|
||||
self.vmctx_globals_begin()
|
||||
.checked_add(
|
||||
index
|
||||
.as_u32()
|
||||
.checked_mul(u32::from(self.size_of_vmglobal_definition()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to the `body` field in `*const VMFunctionBody` index `index`.
|
||||
pub fn vmctx_vmfunction_import_body(&self, index: FuncIndex) -> u32 {
|
||||
self.vmctx_vmfunction_import(index)
|
||||
.checked_add(u32::from(self.vmfunction_import_body()))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to the `vmctx` field in `*const VMFunctionBody` index `index`.
|
||||
pub fn vmctx_vmfunction_import_vmctx(&self, index: FuncIndex) -> u32 {
|
||||
self.vmctx_vmfunction_import(index)
|
||||
.checked_add(u32::from(self.vmfunction_import_vmctx()))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to the `from` field in `VMTableImport` index `index`.
|
||||
pub fn vmctx_vmtable_import_from(&self, index: TableIndex) -> u32 {
|
||||
self.vmctx_vmtable_import(index)
|
||||
.checked_add(u32::from(self.vmtable_import_from()))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to the `base` field in `VMTableDefinition` index `index`.
|
||||
pub fn vmctx_vmtable_definition_base(&self, index: DefinedTableIndex) -> u32 {
|
||||
self.vmctx_vmtable_definition(index)
|
||||
.checked_add(u32::from(self.vmtable_definition_base()))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to the `current_elements` field in `VMTableDefinition` index `index`.
|
||||
pub fn vmctx_vmtable_definition_current_elements(&self, index: DefinedTableIndex) -> u32 {
|
||||
self.vmctx_vmtable_definition(index)
|
||||
.checked_add(u32::from(self.vmtable_definition_current_elements()))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to the `from` field in `VMMemoryImport` index `index`.
|
||||
pub fn vmctx_vmmemory_import_from(&self, index: MemoryIndex) -> u32 {
|
||||
self.vmctx_vmmemory_import(index)
|
||||
.checked_add(u32::from(self.vmmemory_import_from()))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to the `vmctx` field in `VMMemoryImport` index `index`.
|
||||
pub fn vmctx_vmmemory_import_vmctx(&self, index: MemoryIndex) -> u32 {
|
||||
self.vmctx_vmmemory_import(index)
|
||||
.checked_add(u32::from(self.vmmemory_import_vmctx()))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to the `base` field in `VMMemoryDefinition` index `index`.
|
||||
pub fn vmctx_vmmemory_definition_base(&self, index: DefinedMemoryIndex) -> u32 {
|
||||
self.vmctx_vmmemory_definition(index)
|
||||
.checked_add(u32::from(self.vmmemory_definition_base()))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to the `current_length` field in `VMMemoryDefinition` index `index`.
|
||||
pub fn vmctx_vmmemory_definition_current_length(&self, index: DefinedMemoryIndex) -> u32 {
|
||||
self.vmctx_vmmemory_definition(index)
|
||||
.checked_add(u32::from(self.vmmemory_definition_current_length()))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to the `from` field in `VMGlobalImport` index `index`.
|
||||
pub fn vmctx_vmglobal_import_from(&self, index: GlobalIndex) -> u32 {
|
||||
self.vmctx_vmglobal_import(index)
|
||||
.checked_add(u32::from(self.vmglobal_import_from()))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Return the offset to builtin function in `VMBuiltinFunctionsArray` index `index`.
|
||||
pub fn vmctx_builtin_function(&self, index: BuiltinFunctionIndex) -> u32 {
|
||||
self.vmctx_builtin_functions_begin()
|
||||
.checked_add(
|
||||
index
|
||||
.index()
|
||||
.checked_mul(u32::from(self.pointer_size))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Target specific type for shared signature index.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct TargetSharedSignatureIndex(u32);
|
||||
|
||||
impl TargetSharedSignatureIndex {
|
||||
/// Constructs `TargetSharedSignatureIndex`.
|
||||
pub fn new(value: u32) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
|
||||
/// Returns index value.
|
||||
pub fn index(self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::vmoffsets::align;
|
||||
|
||||
#[test]
|
||||
fn alignment() {
|
||||
fn is_aligned(x: u32) -> bool {
|
||||
x % 16 == 0
|
||||
}
|
||||
assert!(is_aligned(align(0, 16)));
|
||||
assert!(is_aligned(align(32, 16)));
|
||||
assert!(is_aligned(align(33, 16)));
|
||||
assert!(is_aligned(align(31, 16)));
|
||||
}
|
||||
}
|
||||
10
crates/environ/tests/cache_default_config_in_memory.rs
Normal file
10
crates/environ/tests/cache_default_config_in_memory.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use wasmtime_environ::cache_init;
|
||||
|
||||
#[test]
|
||||
fn test_cache_default_config_in_memory() {
|
||||
let errors = cache_init::<&str>(true, None, None);
|
||||
assert!(
|
||||
errors.is_empty(),
|
||||
"This test loads config from the default location, if there's one. Make sure it's correct!"
|
||||
);
|
||||
}
|
||||
7
crates/environ/tests/cache_disabled.rs
Normal file
7
crates/environ/tests/cache_disabled.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
use wasmtime_environ::cache_init;
|
||||
|
||||
#[test]
|
||||
fn test_cache_disabled() {
|
||||
let errors = cache_init::<&str>(false, None, None);
|
||||
assert!(errors.is_empty(), "Failed to disable cache system");
|
||||
}
|
||||
26
crates/environ/tests/cache_fail_calling_init_twice.rs
Normal file
26
crates/environ/tests/cache_fail_calling_init_twice.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use std::fs;
|
||||
use tempfile;
|
||||
use wasmtime_environ::cache_init;
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_cache_fail_calling_init_twice() {
|
||||
let dir = tempfile::tempdir().expect("Can't create temporary directory");
|
||||
let cache_dir = dir.path().join("cache-dir");
|
||||
let baseline_compression_level = 5;
|
||||
|
||||
let config_path = dir.path().join("cache-config.toml");
|
||||
let config_content = format!(
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {}\n\
|
||||
baseline-compression-level = {}\n",
|
||||
toml::to_string_pretty(&format!("{}", cache_dir.display())).unwrap(),
|
||||
baseline_compression_level,
|
||||
);
|
||||
fs::write(&config_path, config_content).expect("Failed to write test config file");
|
||||
|
||||
let errors = cache_init(true, Some(&config_path), None);
|
||||
assert!(errors.is_empty());
|
||||
let _errors = cache_init(true, Some(&config_path), None);
|
||||
}
|
||||
23
crates/environ/tests/cache_fail_invalid_config.rs
Normal file
23
crates/environ/tests/cache_fail_invalid_config.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use std::fs;
|
||||
use tempfile;
|
||||
use wasmtime_environ::cache_init;
|
||||
|
||||
#[test]
|
||||
fn test_cache_fail_invalid_config() {
|
||||
let dir = tempfile::tempdir().expect("Can't create temporary directory");
|
||||
let baseline_compression_level = -4;
|
||||
|
||||
let config_path = dir.path().join("cache-config.toml");
|
||||
let config_content = format!(
|
||||
"[cache]\n\
|
||||
enabled = true\n\
|
||||
directory = {}\n\
|
||||
baseline-compression-level = {}\n",
|
||||
toml::to_string_pretty(&format!("{}", config_path.display())).unwrap(), // directory is a file -- incorrect!
|
||||
baseline_compression_level,
|
||||
);
|
||||
fs::write(&config_path, config_content).expect("Failed to write test config file");
|
||||
|
||||
let errors = cache_init(true, Some(&config_path), None);
|
||||
assert!(!errors.is_empty());
|
||||
}
|
||||
10
crates/environ/tests/cache_fail_invalid_path_to_config.rs
Normal file
10
crates/environ/tests/cache_fail_invalid_path_to_config.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use tempfile;
|
||||
use wasmtime_environ::cache_init;
|
||||
|
||||
#[test]
|
||||
fn test_cache_fail_invalid_path_to_config() {
|
||||
let dir = tempfile::tempdir().expect("Can't create temporary directory");
|
||||
let config_path = dir.path().join("cache-config.toml"); // doesn't exist
|
||||
let errors = cache_init(true, Some(&config_path), None);
|
||||
assert!(!errors.is_empty());
|
||||
}
|
||||
13
crates/environ/tests/cache_write_default_config.rs
Normal file
13
crates/environ/tests/cache_write_default_config.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use tempfile;
|
||||
use wasmtime_environ::cache_create_new_config;
|
||||
|
||||
#[test]
|
||||
fn test_cache_write_default_config() {
|
||||
let dir = tempfile::tempdir().expect("Can't create temporary directory");
|
||||
let config_path = dir.path().join("cache-config.toml");
|
||||
|
||||
let result = cache_create_new_config(Some(&config_path));
|
||||
assert!(result.is_ok());
|
||||
assert!(config_path.exists());
|
||||
assert_eq!(config_path, result.unwrap());
|
||||
}
|
||||
20
crates/interface-types/Cargo.toml
Normal file
20
crates/interface-types/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "wasmtime-interface-types"
|
||||
version = "0.2.0"
|
||||
authors = ["The Wasmtime Project Developers"]
|
||||
description = "Support for wasm interface types with wasmtime"
|
||||
categories = ["wasm"]
|
||||
keywords = ["webassembly", "wasm"]
|
||||
repository = "https://github.com/CraneStation/wasmtime"
|
||||
license = "Apache-2.0 WITH LLVM-exception"
|
||||
readme = "README.md"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.19"
|
||||
cranelift-codegen = { version = "0.49", default-features = false }
|
||||
walrus = "0.13"
|
||||
wasmparser = { version = "0.39.2", default-features = false }
|
||||
wasm-webidl-bindings = "0.6"
|
||||
wasmtime-jit = { path = '../jit', default-features = false }
|
||||
wasmtime-runtime = { path = '../runtime', default-features = false }
|
||||
469
crates/interface-types/src/lib.rs
Normal file
469
crates/interface-types/src/lib.rs
Normal file
@@ -0,0 +1,469 @@
|
||||
//! A small crate to handle WebAssembly interface types in wasmtime.
|
||||
//!
|
||||
//! Note that this is intended to follow the [official proposal][proposal] and
|
||||
//! is highly susceptible to change/breakage/etc.
|
||||
//!
|
||||
//! [proposal]: https://github.com/webassembly/webidl-bindings
|
||||
|
||||
#![deny(missing_docs)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate alloc;
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use alloc::string::ToString;
|
||||
use alloc::vec::Vec;
|
||||
use anyhow::{bail, format_err, Result};
|
||||
use core::convert::TryFrom;
|
||||
use core::slice;
|
||||
use core::str;
|
||||
use cranelift_codegen::ir;
|
||||
use wasm_webidl_bindings::ast;
|
||||
use wasmtime_jit::{ActionOutcome, Context, RuntimeValue};
|
||||
use wasmtime_runtime::{Export, InstanceHandle};
|
||||
|
||||
mod value;
|
||||
pub use value::Value;
|
||||
|
||||
/// A data structure intended to hold a parsed representation of the wasm
|
||||
/// interface types of a module.
|
||||
///
|
||||
/// The expected usage pattern is to create this next to wasmtime data
|
||||
/// structures and then use this to process arguments into wasm arguments as
|
||||
/// appropriate for bound functions.
|
||||
pub struct ModuleData {
|
||||
inner: Option<Inner>,
|
||||
}
|
||||
|
||||
struct Inner {
|
||||
module: walrus::Module,
|
||||
}
|
||||
|
||||
/// Representation of a binding of an exported function.
|
||||
///
|
||||
/// Can be used to learn about binding expressions and/or binding types.
|
||||
pub struct ExportBinding<'a> {
|
||||
kind: ExportBindingKind<'a>,
|
||||
}
|
||||
|
||||
enum ExportBindingKind<'a> {
|
||||
Rich {
|
||||
section: &'a ast::WebidlBindings,
|
||||
binding: &'a ast::ExportBinding,
|
||||
},
|
||||
Raw(ir::Signature),
|
||||
}
|
||||
|
||||
impl ModuleData {
|
||||
/// Parses a raw binary wasm file, extracting information about wasm
|
||||
/// interface types.
|
||||
///
|
||||
/// Returns an error if the wasm file is malformed.
|
||||
pub fn new(wasm: &[u8]) -> Result<ModuleData> {
|
||||
// Perform a fast search through the module for the right custom
|
||||
// section. Actually parsing out the interface types data is currently a
|
||||
// pretty expensive operation so we want to only do that if we actually
|
||||
// find the right section.
|
||||
let mut reader = wasmparser::ModuleReader::new(wasm)?;
|
||||
let mut found = false;
|
||||
while !reader.eof() {
|
||||
let section = reader.read()?;
|
||||
if let wasmparser::SectionCode::Custom { name, .. } = section.code {
|
||||
if name == "webidl-bindings" {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return Ok(ModuleData { inner: None });
|
||||
}
|
||||
|
||||
// Ok, perform the more expensive parsing. WebAssembly interface types
|
||||
// are super experimental and under development. To get something
|
||||
// quickly up and running we're using the same crate as `wasm-bindgen`,
|
||||
// a producer of wasm interface types, the `wasm-webidl-bindings` crate.
|
||||
// This crate relies on `walrus` which has its own IR for a wasm module.
|
||||
// Ideally we'd do all this during cranelift's own parsing of the wasm
|
||||
// module and we wouldn't have to reparse here purely for this one use
|
||||
// case.
|
||||
//
|
||||
// For now though this is "fast enough" and good enough for some demos,
|
||||
// but for full-on production quality engines we'll want to integrate
|
||||
// this much more tightly with the rest of wasmtime.
|
||||
let module = walrus::ModuleConfig::new()
|
||||
.on_parse(wasm_webidl_bindings::binary::on_parse)
|
||||
.parse(wasm)?;
|
||||
|
||||
Ok(ModuleData {
|
||||
inner: Some(Inner { module }),
|
||||
})
|
||||
}
|
||||
|
||||
/// Detects if WASI support is needed: returns module name that is requested.
|
||||
pub fn find_wasi_module_name(&self) -> Option<String> {
|
||||
self.inner.as_ref().and_then(|Inner { module }| {
|
||||
module
|
||||
.imports
|
||||
.iter()
|
||||
.find(|walrus::Import { module, .. }| match module.as_str() {
|
||||
"wasi" | "wasi_unstable" => true,
|
||||
_ => false,
|
||||
})
|
||||
.map(|walrus::Import { module, .. }| module.clone())
|
||||
})
|
||||
}
|
||||
|
||||
/// Same as `Context::invoke` except that this works with a `&[Value]` list
|
||||
/// instead of a `&[RuntimeValue]` list. (in this case `Value` is the set of
|
||||
/// wasm interface types)
|
||||
pub fn invoke(
|
||||
&self,
|
||||
cx: &mut Context,
|
||||
handle: &mut InstanceHandle,
|
||||
export: &str,
|
||||
args: &[Value],
|
||||
) -> Result<Vec<Value>> {
|
||||
let binding = self.binding_for_export(handle, export)?;
|
||||
let incoming = binding.param_bindings()?;
|
||||
let outgoing = binding.result_bindings()?;
|
||||
|
||||
let wasm_args = translate_incoming(cx, handle, &incoming, args)?;
|
||||
let wasm_results = match cx.invoke(handle, export, &wasm_args)? {
|
||||
ActionOutcome::Returned { values } => values,
|
||||
ActionOutcome::Trapped { message } => bail!("trapped: {}", message),
|
||||
};
|
||||
translate_outgoing(cx, handle, &outgoing, &wasm_results)
|
||||
}
|
||||
|
||||
/// Returns an appropriate binding for the `name` export in this module
|
||||
/// which has also been instantiated as `instance` provided here.
|
||||
///
|
||||
/// Returns an error if `name` is not present in the module.
|
||||
pub fn binding_for_export(
|
||||
&self,
|
||||
instance: &mut InstanceHandle,
|
||||
name: &str,
|
||||
) -> Result<ExportBinding<'_>> {
|
||||
if let Some(binding) = self.interface_binding_for_export(name) {
|
||||
return Ok(binding);
|
||||
}
|
||||
let signature = match instance.lookup(name) {
|
||||
Some(Export::Function { signature, .. }) => signature,
|
||||
Some(_) => bail!("`{}` is not a function", name),
|
||||
None => bail!("failed to find export `{}`", name),
|
||||
};
|
||||
Ok(ExportBinding {
|
||||
kind: ExportBindingKind::Raw(signature),
|
||||
})
|
||||
}
|
||||
|
||||
fn interface_binding_for_export(&self, name: &str) -> Option<ExportBinding<'_>> {
|
||||
let inner = self.inner.as_ref()?;
|
||||
let bindings = inner.module.customs.get_typed::<ast::WebidlBindings>()?;
|
||||
let export = inner.module.exports.iter().find(|e| e.name == name)?;
|
||||
let id = match export.item {
|
||||
walrus::ExportItem::Function(f) => f,
|
||||
_ => panic!(),
|
||||
};
|
||||
let (_, bind) = bindings.binds.iter().find(|(_, b)| b.func == id)?;
|
||||
let binding = bindings.bindings.get(bind.binding)?;
|
||||
let binding = match binding {
|
||||
ast::FunctionBinding::Export(export) => export,
|
||||
ast::FunctionBinding::Import(_) => return None,
|
||||
};
|
||||
Some(ExportBinding {
|
||||
kind: ExportBindingKind::Rich {
|
||||
binding,
|
||||
section: bindings,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ExportBinding<'_> {
|
||||
/// Returns the list of binding expressions used to create the parameters
|
||||
/// for this binding.
|
||||
pub fn param_bindings(&self) -> Result<Vec<ast::IncomingBindingExpression>> {
|
||||
match &self.kind {
|
||||
ExportBindingKind::Rich { binding, .. } => Ok(binding.params.bindings.clone()),
|
||||
ExportBindingKind::Raw(sig) => sig
|
||||
.params
|
||||
.iter()
|
||||
.skip(1) // skip the VMContext argument
|
||||
.enumerate()
|
||||
.map(|(i, param)| default_incoming(i, param))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the list of scalar types used for this binding
|
||||
pub fn param_types(&self) -> Result<Vec<ast::WebidlScalarType>> {
|
||||
match &self.kind {
|
||||
ExportBindingKind::Rich {
|
||||
binding, section, ..
|
||||
} => {
|
||||
let id = match binding.webidl_ty {
|
||||
ast::WebidlTypeRef::Id(id) => id,
|
||||
ast::WebidlTypeRef::Scalar(_) => {
|
||||
bail!("webidl types for functions cannot be scalar")
|
||||
}
|
||||
};
|
||||
let ty = section
|
||||
.types
|
||||
.get::<ast::WebidlCompoundType>(id)
|
||||
.ok_or_else(|| format_err!("invalid webidl custom section"))?;
|
||||
let func = match ty {
|
||||
ast::WebidlCompoundType::Function(f) => f,
|
||||
_ => bail!("webidl type for function must be of function type"),
|
||||
};
|
||||
func.params
|
||||
.iter()
|
||||
.map(|param| match param {
|
||||
ast::WebidlTypeRef::Id(_) => bail!("function arguments cannot be compound"),
|
||||
ast::WebidlTypeRef::Scalar(s) => Ok(*s),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
ExportBindingKind::Raw(sig) => sig.params.iter().skip(1).map(abi2ast).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the list of binding expressions used to extract the return
|
||||
/// values of this binding.
|
||||
pub fn result_bindings(&self) -> Result<Vec<ast::OutgoingBindingExpression>> {
|
||||
match &self.kind {
|
||||
ExportBindingKind::Rich { binding, .. } => Ok(binding.result.bindings.clone()),
|
||||
ExportBindingKind::Raw(sig) => sig
|
||||
.returns
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, param)| default_outgoing(i, param))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_incoming(idx: usize, param: &ir::AbiParam) -> Result<ast::IncomingBindingExpression> {
|
||||
let get = ast::IncomingBindingExpressionGet { idx: idx as u32 };
|
||||
let ty = if param.value_type == ir::types::I32 {
|
||||
walrus::ValType::I32
|
||||
} else if param.value_type == ir::types::I64 {
|
||||
walrus::ValType::I64
|
||||
} else if param.value_type == ir::types::F32 {
|
||||
walrus::ValType::F32
|
||||
} else if param.value_type == ir::types::F64 {
|
||||
walrus::ValType::F64
|
||||
} else {
|
||||
bail!("unsupported type {:?}", param.value_type)
|
||||
};
|
||||
Ok(ast::IncomingBindingExpressionAs {
|
||||
ty,
|
||||
expr: Box::new(get.into()),
|
||||
}
|
||||
.into())
|
||||
}
|
||||
|
||||
fn default_outgoing(idx: usize, param: &ir::AbiParam) -> Result<ast::OutgoingBindingExpression> {
|
||||
let ty = abi2ast(param)?;
|
||||
Ok(ast::OutgoingBindingExpressionAs {
|
||||
ty: ty.into(),
|
||||
idx: idx as u32,
|
||||
}
|
||||
.into())
|
||||
}
|
||||
|
||||
fn abi2ast(param: &ir::AbiParam) -> Result<ast::WebidlScalarType> {
|
||||
Ok(if param.value_type == ir::types::I32 {
|
||||
ast::WebidlScalarType::Long
|
||||
} else if param.value_type == ir::types::I64 {
|
||||
ast::WebidlScalarType::LongLong
|
||||
} else if param.value_type == ir::types::F32 {
|
||||
ast::WebidlScalarType::UnrestrictedFloat
|
||||
} else if param.value_type == ir::types::F64 {
|
||||
ast::WebidlScalarType::UnrestrictedDouble
|
||||
} else {
|
||||
bail!("unsupported type {:?}", param.value_type)
|
||||
})
|
||||
}
|
||||
|
||||
fn translate_incoming(
|
||||
cx: &mut Context,
|
||||
handle: &mut InstanceHandle,
|
||||
bindings: &[ast::IncomingBindingExpression],
|
||||
args: &[Value],
|
||||
) -> Result<Vec<RuntimeValue>> {
|
||||
let get = |expr: &ast::IncomingBindingExpression| match expr {
|
||||
ast::IncomingBindingExpression::Get(g) => args
|
||||
.get(g.idx as usize)
|
||||
.ok_or_else(|| format_err!("argument index out of bounds: {}", g.idx)),
|
||||
_ => bail!("unsupported incoming binding expr {:?}", expr),
|
||||
};
|
||||
|
||||
let mut copy = |alloc_func_name: &str, bytes: &[u8]| {
|
||||
let len = i32::try_from(bytes.len()).map_err(|_| format_err!("length overflow"))?;
|
||||
let alloc_args = vec![RuntimeValue::I32(len)];
|
||||
let results = match cx.invoke(handle, alloc_func_name, &alloc_args)? {
|
||||
ActionOutcome::Returned { values } => values,
|
||||
ActionOutcome::Trapped { message } => bail!("trapped: {}", message),
|
||||
};
|
||||
if results.len() != 1 {
|
||||
bail!("allocator function wrong number of results");
|
||||
}
|
||||
let ptr = match results[0] {
|
||||
RuntimeValue::I32(i) => i,
|
||||
_ => bail!("allocator function bad return type"),
|
||||
};
|
||||
let memory = handle
|
||||
.lookup("memory")
|
||||
.ok_or_else(|| format_err!("no exported `memory`"))?;
|
||||
let definition = match memory {
|
||||
wasmtime_runtime::Export::Memory { definition, .. } => definition,
|
||||
_ => bail!("export `memory` wasn't a `Memory`"),
|
||||
};
|
||||
unsafe {
|
||||
let raw = slice::from_raw_parts_mut((*definition).base, (*definition).current_length);
|
||||
raw[ptr as usize..][..bytes.len()].copy_from_slice(bytes)
|
||||
}
|
||||
|
||||
Ok((ptr, len))
|
||||
};
|
||||
|
||||
let mut wasm = Vec::new();
|
||||
|
||||
for expr in bindings {
|
||||
match expr {
|
||||
ast::IncomingBindingExpression::AllocUtf8Str(g) => {
|
||||
let val = match get(&g.expr)? {
|
||||
Value::String(s) => s,
|
||||
_ => bail!("expected a string"),
|
||||
};
|
||||
let (ptr, len) = copy(&g.alloc_func_name, val.as_bytes())?;
|
||||
wasm.push(RuntimeValue::I32(ptr));
|
||||
wasm.push(RuntimeValue::I32(len));
|
||||
}
|
||||
ast::IncomingBindingExpression::As(g) => {
|
||||
let val = get(&g.expr)?;
|
||||
match g.ty {
|
||||
walrus::ValType::I32 => match val {
|
||||
Value::I32(i) => wasm.push(RuntimeValue::I32(*i)),
|
||||
Value::U32(i) => wasm.push(RuntimeValue::I32(*i as i32)),
|
||||
_ => bail!("cannot convert {:?} to `i32`", val),
|
||||
},
|
||||
walrus::ValType::I64 => match val {
|
||||
Value::I32(i) => wasm.push(RuntimeValue::I64((*i).into())),
|
||||
Value::U32(i) => wasm.push(RuntimeValue::I64((*i).into())),
|
||||
Value::I64(i) => wasm.push(RuntimeValue::I64(*i)),
|
||||
Value::U64(i) => wasm.push(RuntimeValue::I64(*i as i64)),
|
||||
_ => bail!("cannot convert {:?} to `i64`", val),
|
||||
},
|
||||
walrus::ValType::F32 => match val {
|
||||
Value::F32(i) => wasm.push(RuntimeValue::F32(i.to_bits())),
|
||||
_ => bail!("cannot convert {:?} to `f32`", val),
|
||||
},
|
||||
walrus::ValType::F64 => match val {
|
||||
Value::F32(i) => wasm.push(RuntimeValue::F64((*i as f64).to_bits())),
|
||||
Value::F64(i) => wasm.push(RuntimeValue::F64(i.to_bits())),
|
||||
_ => bail!("cannot convert {:?} to `f64`", val),
|
||||
},
|
||||
walrus::ValType::V128 | walrus::ValType::Anyref => {
|
||||
bail!("unsupported `as` type {:?}", g.ty);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => bail!("unsupported incoming binding expr {:?}", expr),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(wasm)
|
||||
}
|
||||
|
||||
fn translate_outgoing(
|
||||
cx: &mut Context,
|
||||
handle: &mut InstanceHandle,
|
||||
bindings: &[ast::OutgoingBindingExpression],
|
||||
args: &[RuntimeValue],
|
||||
) -> Result<Vec<Value>> {
|
||||
let mut values = Vec::new();
|
||||
|
||||
let raw_memory = || unsafe {
|
||||
let memory = handle
|
||||
.lookup_immutable("memory")
|
||||
.ok_or_else(|| format_err!("no exported `memory`"))?;
|
||||
let definition = match memory {
|
||||
wasmtime_runtime::Export::Memory { definition, .. } => definition,
|
||||
_ => bail!("export `memory` wasn't a `Memory`"),
|
||||
};
|
||||
Ok(slice::from_raw_parts_mut(
|
||||
(*definition).base,
|
||||
(*definition).current_length,
|
||||
))
|
||||
};
|
||||
|
||||
let get = |idx: u32| {
|
||||
args.get(idx as usize)
|
||||
.cloned()
|
||||
.ok_or_else(|| format_err!("argument index out of bounds: {}", idx))
|
||||
};
|
||||
|
||||
for expr in bindings {
|
||||
match expr {
|
||||
ast::OutgoingBindingExpression::As(a) => {
|
||||
let arg = get(a.idx)?;
|
||||
match a.ty {
|
||||
ast::WebidlTypeRef::Scalar(ast::WebidlScalarType::UnsignedLong) => match arg {
|
||||
RuntimeValue::I32(a) => values.push(Value::U32(a as u32)),
|
||||
_ => bail!("can't convert {:?} to unsigned long", arg),
|
||||
},
|
||||
ast::WebidlTypeRef::Scalar(ast::WebidlScalarType::Long) => match arg {
|
||||
RuntimeValue::I32(a) => values.push(Value::I32(a)),
|
||||
_ => bail!("can't convert {:?} to long", arg),
|
||||
},
|
||||
ast::WebidlTypeRef::Scalar(ast::WebidlScalarType::LongLong) => match arg {
|
||||
RuntimeValue::I32(a) => values.push(Value::I64(a as i64)),
|
||||
RuntimeValue::I64(a) => values.push(Value::I64(a)),
|
||||
_ => bail!("can't convert {:?} to long long", arg),
|
||||
},
|
||||
ast::WebidlTypeRef::Scalar(ast::WebidlScalarType::UnsignedLongLong) => {
|
||||
match arg {
|
||||
RuntimeValue::I32(a) => values.push(Value::U64(a as u64)),
|
||||
RuntimeValue::I64(a) => values.push(Value::U64(a as u64)),
|
||||
_ => bail!("can't convert {:?} to unsigned long long", arg),
|
||||
}
|
||||
}
|
||||
ast::WebidlTypeRef::Scalar(ast::WebidlScalarType::Float) => match arg {
|
||||
RuntimeValue::F32(a) => values.push(Value::F32(f32::from_bits(a))),
|
||||
_ => bail!("can't convert {:?} to float", arg),
|
||||
},
|
||||
ast::WebidlTypeRef::Scalar(ast::WebidlScalarType::Double) => match arg {
|
||||
RuntimeValue::F32(a) => values.push(Value::F64(f32::from_bits(a) as f64)),
|
||||
RuntimeValue::F64(a) => values.push(Value::F64(f64::from_bits(a))),
|
||||
_ => bail!("can't convert {:?} to double", arg),
|
||||
},
|
||||
_ => bail!("unsupported outgoing binding expr {:?}", expr),
|
||||
}
|
||||
}
|
||||
ast::OutgoingBindingExpression::Utf8Str(e) => {
|
||||
if e.ty != ast::WebidlScalarType::DomString.into() {
|
||||
bail!("utf-8 strings must go into dom-string")
|
||||
}
|
||||
let offset = match get(e.offset)? {
|
||||
RuntimeValue::I32(a) => a,
|
||||
_ => bail!("offset must be an i32"),
|
||||
};
|
||||
let length = match get(e.length)? {
|
||||
RuntimeValue::I32(a) => a,
|
||||
_ => bail!("length must be an i32"),
|
||||
};
|
||||
let bytes = &raw_memory()?[offset as usize..][..length as usize];
|
||||
values.push(Value::String(str::from_utf8(bytes).unwrap().to_string()));
|
||||
}
|
||||
_ => {
|
||||
drop((cx, handle));
|
||||
bail!("unsupported outgoing binding expr {:?}", expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(values)
|
||||
}
|
||||
67
crates/interface-types/src/value.rs
Normal file
67
crates/interface-types/src/value.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use alloc::string::{String, ToString};
|
||||
use core::convert::TryFrom;
|
||||
use core::fmt;
|
||||
|
||||
/// The set of all possible WebAssembly Interface Types
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum Value {
|
||||
String(String),
|
||||
I32(i32),
|
||||
U32(u32),
|
||||
I64(i64),
|
||||
U64(u64),
|
||||
F32(f32),
|
||||
F64(f64),
|
||||
}
|
||||
|
||||
macro_rules! from {
|
||||
($($a:ident => $b:ident,)*) => ($(
|
||||
impl From<$a> for Value {
|
||||
fn from(val: $a) -> Value {
|
||||
Value::$b(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Value> for $a {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(val: Value) -> Result<$a, Self::Error> {
|
||||
match val {
|
||||
Value::$b(v) => Ok(v),
|
||||
v => anyhow::bail!("cannot convert {:?} to {}", v, stringify!($a)),
|
||||
}
|
||||
}
|
||||
}
|
||||
)*)
|
||||
}
|
||||
|
||||
from! {
|
||||
String => String,
|
||||
i32 => I32,
|
||||
u32 => U32,
|
||||
i64 => I64,
|
||||
u64 => U64,
|
||||
f32 => F32,
|
||||
f64 => F64,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Value {
|
||||
fn from(x: &'a str) -> Value {
|
||||
x.to_string().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Value {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Value::String(s) => s.fmt(f),
|
||||
Value::I32(s) => s.fmt(f),
|
||||
Value::U32(s) => s.fmt(f),
|
||||
Value::I64(s) => s.fmt(f),
|
||||
Value::U64(s) => s.fmt(f),
|
||||
Value::F32(s) => s.fmt(f),
|
||||
Value::F64(s) => s.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
39
crates/jit/Cargo.toml
Normal file
39
crates/jit/Cargo.toml
Normal file
@@ -0,0 +1,39 @@
|
||||
[package]
|
||||
name = "wasmtime-jit"
|
||||
version = "0.2.0"
|
||||
authors = ["The Wasmtime Project Developers"]
|
||||
description = "JIT-style execution for WebAsssembly code in Cranelift"
|
||||
categories = ["wasm"]
|
||||
keywords = ["webassembly", "wasm"]
|
||||
repository = "https://github.com/CraneStation/wasmtime"
|
||||
license = "Apache-2.0 WITH LLVM-exception"
|
||||
readme = "README.md"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
cranelift-codegen = { version = "0.49", features = ["enable-serde"] }
|
||||
cranelift-entity = { version = "0.49", features = ["enable-serde"] }
|
||||
cranelift-wasm = { version = "0.49", features = ["enable-serde"] }
|
||||
cranelift-frontend = { version = "0.49" }
|
||||
wasmtime-environ = { path = "../environ", default-features = false }
|
||||
wasmtime-runtime = { path = "../runtime", default-features = false }
|
||||
wasmtime-debug = { path = "../debug", default-features = false }
|
||||
region = "2.0.0"
|
||||
failure = { version = "0.1.3", default-features = false }
|
||||
thiserror = "1.0.4"
|
||||
target-lexicon = { version = "0.9.0", default-features = false }
|
||||
hashbrown = { version = "0.6.0", optional = true }
|
||||
wasmparser = { version = "0.39.2", default-features = false }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
winapi = { version = "0.3.7", features = ["winnt", "impl-default"] }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = ["cranelift-codegen/std", "cranelift-wasm/std", "wasmtime-environ/std", "wasmtime-debug/std", "wasmtime-runtime/std", "wasmparser/std"]
|
||||
core = ["hashbrown/nightly", "cranelift-codegen/core", "cranelift-wasm/core", "wasmtime-environ/core", "wasmtime-debug/core", "wasmparser/core"]
|
||||
lightbeam = ["wasmtime-environ/lightbeam"]
|
||||
|
||||
[badges]
|
||||
maintenance = { status = "experimental" }
|
||||
travis-ci = { repository = "CraneStation/wasmtime" }
|
||||
220
crates/jit/LICENSE
Normal file
220
crates/jit/LICENSE
Normal file
@@ -0,0 +1,220 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
--- LLVM Exceptions to the Apache 2.0 License ----
|
||||
|
||||
As an exception, if, as a result of your compiling your source code, portions
|
||||
of this Software are embedded into an Object form of such source code, you
|
||||
may redistribute such embedded portions in such Object form without complying
|
||||
with the conditions of Sections 4(a), 4(b) and 4(d) of the License.
|
||||
|
||||
In addition, if you combine or link compiled forms of this Software with
|
||||
software that is licensed under the GPLv2 ("Combined Software") and if a
|
||||
court of competent jurisdiction determines that the patent provision (Section
|
||||
3), the indemnity provision (Section 9) or other Section of the License
|
||||
conflicts with the conditions of the GPLv2, you may retroactively and
|
||||
prospectively choose to deem waived or otherwise exclude such Section(s) of
|
||||
the License, but only in their entirety and only with respect to the Combined
|
||||
Software.
|
||||
|
||||
6
crates/jit/README.md
Normal file
6
crates/jit/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
This is the `wasmtime-jit` crate, which contains JIT-based execution
|
||||
for wasm, using the wasm ABI defined by [`wasmtime-environ`] and the
|
||||
runtime support provided by [`wasmtime-runtime`].
|
||||
|
||||
[`wasmtime-environ`]: https://crates.io/crates/wasmtime-environ
|
||||
[`wasmtime-runtime`]: https://crates.io/crates/wasmtime-runtime
|
||||
298
crates/jit/src/action.rs
Normal file
298
crates/jit/src/action.rs
Normal file
@@ -0,0 +1,298 @@
|
||||
//! Support for performing actions with a wasm module from the outside.
|
||||
|
||||
use crate::compiler::Compiler;
|
||||
use crate::instantiate::SetupError;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use core::cmp::max;
|
||||
use core::{fmt, mem, ptr, slice};
|
||||
use cranelift_codegen::ir;
|
||||
use thiserror::Error;
|
||||
use wasmtime_runtime::{wasmtime_call_trampoline, Export, InstanceHandle, VMInvokeArgument};
|
||||
|
||||
/// A runtime value.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum RuntimeValue {
|
||||
/// A runtime value with type i32.
|
||||
I32(i32),
|
||||
/// A runtime value with type i64.
|
||||
I64(i64),
|
||||
/// A runtime value with type f32.
|
||||
F32(u32),
|
||||
/// A runtime value with type f64.
|
||||
F64(u64),
|
||||
/// A runtime value with type v128
|
||||
V128([u8; 16]),
|
||||
}
|
||||
|
||||
impl RuntimeValue {
|
||||
/// Return the type of this `RuntimeValue`.
|
||||
pub fn value_type(self) -> ir::Type {
|
||||
match self {
|
||||
Self::I32(_) => ir::types::I32,
|
||||
Self::I64(_) => ir::types::I64,
|
||||
Self::F32(_) => ir::types::F32,
|
||||
Self::F64(_) => ir::types::F64,
|
||||
Self::V128(_) => ir::types::I8X16,
|
||||
}
|
||||
}
|
||||
|
||||
/// Assuming this `RuntimeValue` holds an `i32`, return that value.
|
||||
pub fn unwrap_i32(self) -> i32 {
|
||||
match self {
|
||||
Self::I32(x) => x,
|
||||
_ => panic!("unwrapping value of type {} as i32", self.value_type()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Assuming this `RuntimeValue` holds an `i64`, return that value.
|
||||
pub fn unwrap_i64(self) -> i64 {
|
||||
match self {
|
||||
Self::I64(x) => x,
|
||||
_ => panic!("unwrapping value of type {} as i64", self.value_type()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Assuming this `RuntimeValue` holds an `f32`, return that value.
|
||||
pub fn unwrap_f32(self) -> f32 {
|
||||
f32::from_bits(self.unwrap_f32_bits())
|
||||
}
|
||||
|
||||
/// Assuming this `RuntimeValue` holds an `f32`, return the bits of that value as a `u32`.
|
||||
pub fn unwrap_f32_bits(self) -> u32 {
|
||||
match self {
|
||||
Self::F32(x) => x,
|
||||
_ => panic!("unwrapping value of type {} as f32", self.value_type()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Assuming this `RuntimeValue` holds an `f64`, return that value.
|
||||
pub fn unwrap_f64(self) -> f64 {
|
||||
f64::from_bits(self.unwrap_f64_bits())
|
||||
}
|
||||
|
||||
/// Assuming this `RuntimeValue` holds an `f64`, return the bits of that value as a `u64`.
|
||||
pub fn unwrap_f64_bits(self) -> u64 {
|
||||
match self {
|
||||
Self::F64(x) => x,
|
||||
_ => panic!("unwrapping value of type {} as f64", self.value_type()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for RuntimeValue {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::I32(x) => write!(f, "{}: i32", x),
|
||||
Self::I64(x) => write!(f, "{}: i64", x),
|
||||
Self::F32(x) => write!(f, "{}: f32", x),
|
||||
Self::F64(x) => write!(f, "{}: f64", x),
|
||||
Self::V128(x) => write!(f, "{:?}: v128", x.to_vec()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of invoking a wasm function or reading a wasm global.
|
||||
#[derive(Debug)]
|
||||
pub enum ActionOutcome {
|
||||
/// The action returned normally. Its return values are provided.
|
||||
Returned {
|
||||
/// The return values.
|
||||
values: Vec<RuntimeValue>,
|
||||
},
|
||||
|
||||
/// A trap occurred while the action was executing.
|
||||
Trapped {
|
||||
/// The trap message.
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// An error detected while invoking a wasm function or reading a wasm global.
|
||||
/// Note that at this level, traps are not reported errors, but are rather
|
||||
/// returned through `ActionOutcome`.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ActionError {
|
||||
/// An internal implementation error occurred.
|
||||
#[error("{0}")]
|
||||
Setup(#[from] SetupError),
|
||||
|
||||
/// No field with the specified name was present.
|
||||
#[error("Unknown field: {0}")]
|
||||
Field(String),
|
||||
|
||||
/// The field was present but was the wrong kind (eg. function, table, global, or memory).
|
||||
#[error("Kind error: {0}")]
|
||||
Kind(String),
|
||||
|
||||
/// The field was present but was the wrong type (eg. i32, i64, f32, or f64).
|
||||
#[error("Type error: {0}")]
|
||||
Type(String),
|
||||
}
|
||||
|
||||
/// Invoke a function through an `InstanceHandle` identified by an export name.
|
||||
pub fn invoke(
|
||||
compiler: &mut Compiler,
|
||||
instance: &mut InstanceHandle,
|
||||
function_name: &str,
|
||||
args: &[RuntimeValue],
|
||||
) -> Result<ActionOutcome, ActionError> {
|
||||
let (address, signature, callee_vmctx) = match instance.lookup(function_name) {
|
||||
Some(Export::Function {
|
||||
address,
|
||||
signature,
|
||||
vmctx,
|
||||
}) => (address, signature, vmctx),
|
||||
Some(_) => {
|
||||
return Err(ActionError::Kind(format!(
|
||||
"exported item \"{}\" is not a function",
|
||||
function_name
|
||||
)));
|
||||
}
|
||||
None => {
|
||||
return Err(ActionError::Field(format!(
|
||||
"no export named \"{}\"",
|
||||
function_name
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
for (index, value) in args.iter().enumerate() {
|
||||
// Add one to account for the leading vmctx argument.
|
||||
assert_eq!(value.value_type(), signature.params[index + 1].value_type);
|
||||
}
|
||||
|
||||
// TODO: Support values larger than v128. And pack the values into memory
|
||||
// instead of just using fixed-sized slots.
|
||||
// Subtract one becase we don't pass the vmctx argument in `values_vec`.
|
||||
let value_size = mem::size_of::<VMInvokeArgument>();
|
||||
let mut values_vec: Vec<VMInvokeArgument> =
|
||||
vec![VMInvokeArgument::new(); max(signature.params.len() - 1, signature.returns.len())];
|
||||
|
||||
// Store the argument values into `values_vec`.
|
||||
for (index, arg) in args.iter().enumerate() {
|
||||
unsafe {
|
||||
let ptr = values_vec.as_mut_ptr().add(index);
|
||||
|
||||
match arg {
|
||||
RuntimeValue::I32(x) => ptr::write(ptr as *mut i32, *x),
|
||||
RuntimeValue::I64(x) => ptr::write(ptr as *mut i64, *x),
|
||||
RuntimeValue::F32(x) => ptr::write(ptr as *mut u32, *x),
|
||||
RuntimeValue::F64(x) => ptr::write(ptr as *mut u64, *x),
|
||||
RuntimeValue::V128(x) => ptr::write(ptr as *mut [u8; 16], *x),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the trampoline to call for this function.
|
||||
let exec_code_buf = compiler
|
||||
.get_trampoline(address, &signature, value_size)
|
||||
.map_err(ActionError::Setup)?;
|
||||
|
||||
// Make all JIT code produced thus far executable.
|
||||
compiler.publish_compiled_code();
|
||||
|
||||
// Call the trampoline.
|
||||
if let Err(message) = unsafe {
|
||||
wasmtime_call_trampoline(
|
||||
callee_vmctx,
|
||||
exec_code_buf,
|
||||
values_vec.as_mut_ptr() as *mut u8,
|
||||
)
|
||||
} {
|
||||
return Ok(ActionOutcome::Trapped { message });
|
||||
}
|
||||
|
||||
// Load the return values out of `values_vec`.
|
||||
let values = signature
|
||||
.returns
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, abi_param)| unsafe {
|
||||
let ptr = values_vec.as_ptr().add(index);
|
||||
|
||||
match abi_param.value_type {
|
||||
ir::types::I32 => RuntimeValue::I32(ptr::read(ptr as *const i32)),
|
||||
ir::types::I64 => RuntimeValue::I64(ptr::read(ptr as *const i64)),
|
||||
ir::types::F32 => RuntimeValue::F32(ptr::read(ptr as *const u32)),
|
||||
ir::types::F64 => RuntimeValue::F64(ptr::read(ptr as *const u64)),
|
||||
ir::types::I8X16 => RuntimeValue::V128(ptr::read(ptr as *const [u8; 16])),
|
||||
other => panic!("unsupported value type {:?}", other),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(ActionOutcome::Returned { values })
|
||||
}
|
||||
|
||||
/// Returns a slice of the contents of allocated linear memory.
|
||||
pub fn inspect_memory<'instance>(
|
||||
instance: &'instance InstanceHandle,
|
||||
memory_name: &str,
|
||||
start: usize,
|
||||
len: usize,
|
||||
) -> Result<&'instance [u8], ActionError> {
|
||||
let definition = match unsafe { instance.lookup_immutable(memory_name) } {
|
||||
Some(Export::Memory {
|
||||
definition,
|
||||
memory: _memory,
|
||||
vmctx: _vmctx,
|
||||
}) => definition,
|
||||
Some(_) => {
|
||||
return Err(ActionError::Kind(format!(
|
||||
"exported item \"{}\" is not a linear memory",
|
||||
memory_name
|
||||
)));
|
||||
}
|
||||
None => {
|
||||
return Err(ActionError::Field(format!(
|
||||
"no export named \"{}\"",
|
||||
memory_name
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(unsafe {
|
||||
let memory_def = &*definition;
|
||||
&slice::from_raw_parts(memory_def.base, memory_def.current_length)[start..start + len]
|
||||
})
|
||||
}
|
||||
|
||||
/// Read a global in the given instance identified by an export name.
|
||||
pub fn get(instance: &InstanceHandle, global_name: &str) -> Result<RuntimeValue, ActionError> {
|
||||
let (definition, global) = match unsafe { instance.lookup_immutable(global_name) } {
|
||||
Some(Export::Global {
|
||||
definition,
|
||||
vmctx: _,
|
||||
global,
|
||||
}) => (definition, global),
|
||||
Some(_) => {
|
||||
return Err(ActionError::Kind(format!(
|
||||
"exported item \"{}\" is not a global variable",
|
||||
global_name
|
||||
)));
|
||||
}
|
||||
None => {
|
||||
return Err(ActionError::Field(format!(
|
||||
"no export named \"{}\"",
|
||||
global_name
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let global_def = &*definition;
|
||||
Ok(match global.ty {
|
||||
ir::types::I32 => RuntimeValue::I32(*global_def.as_i32()),
|
||||
ir::types::I64 => RuntimeValue::I64(*global_def.as_i64()),
|
||||
ir::types::F32 => RuntimeValue::F32(*global_def.as_f32_bits()),
|
||||
ir::types::F64 => RuntimeValue::F64(*global_def.as_f64_bits()),
|
||||
other => {
|
||||
return Err(ActionError::Type(format!(
|
||||
"global with type {} not supported",
|
||||
other
|
||||
)));
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
195
crates/jit/src/code_memory.rs
Normal file
195
crates/jit/src/code_memory.rs
Normal file
@@ -0,0 +1,195 @@
|
||||
//! Memory management for executable code.
|
||||
|
||||
use crate::function_table::FunctionTable;
|
||||
use alloc::boxed::Box;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use core::{cmp, mem};
|
||||
use region;
|
||||
use wasmtime_environ::{Compilation, CompiledFunction};
|
||||
use wasmtime_runtime::{Mmap, VMFunctionBody};
|
||||
|
||||
/// Memory manager for executable code.
|
||||
pub struct CodeMemory {
|
||||
current: (Mmap, FunctionTable),
|
||||
mmaps: Vec<(Mmap, FunctionTable)>,
|
||||
position: usize,
|
||||
published: usize,
|
||||
}
|
||||
|
||||
impl CodeMemory {
|
||||
/// Create a new `CodeMemory` instance.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
current: (Mmap::new(), FunctionTable::new()),
|
||||
mmaps: Vec::new(),
|
||||
position: 0,
|
||||
published: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Allocate a continuous memory block for a single compiled function.
|
||||
/// TODO: Reorganize the code that calls this to emit code directly into the
|
||||
/// mmap region rather than into a Vec that we need to copy in.
|
||||
pub fn allocate_for_function(
|
||||
&mut self,
|
||||
func: &CompiledFunction,
|
||||
) -> Result<&mut [VMFunctionBody], String> {
|
||||
let size = Self::function_allocation_size(func);
|
||||
|
||||
let start = self.position as u32;
|
||||
let (buf, table) = self.allocate(size)?;
|
||||
|
||||
let (_, _, _, vmfunc) = Self::copy_function(func, start, buf, table);
|
||||
|
||||
Ok(vmfunc)
|
||||
}
|
||||
|
||||
/// Allocate a continuous memory block for a compilation.
|
||||
///
|
||||
/// Allocates memory for both the function bodies as well as function unwind data.
|
||||
pub fn allocate_for_compilation(
|
||||
&mut self,
|
||||
compilation: &Compilation,
|
||||
) -> Result<Box<[&mut [VMFunctionBody]]>, String> {
|
||||
let total_len = compilation
|
||||
.into_iter()
|
||||
.fold(0, |acc, func| acc + Self::function_allocation_size(func));
|
||||
|
||||
let mut start = self.position as u32;
|
||||
let (mut buf, mut table) = self.allocate(total_len)?;
|
||||
let mut result = Vec::with_capacity(compilation.len());
|
||||
|
||||
for func in compilation.into_iter() {
|
||||
let (next_start, next_buf, next_table, vmfunc) =
|
||||
Self::copy_function(func, start, buf, table);
|
||||
|
||||
result.push(vmfunc);
|
||||
|
||||
start = next_start;
|
||||
buf = next_buf;
|
||||
table = next_table;
|
||||
}
|
||||
|
||||
Ok(result.into_boxed_slice())
|
||||
}
|
||||
|
||||
/// Make all allocated memory executable.
|
||||
pub fn publish(&mut self) {
|
||||
self.push_current(0)
|
||||
.expect("failed to push current memory map");
|
||||
|
||||
for (m, t) in &mut self.mmaps[self.published..] {
|
||||
if m.len() != 0 {
|
||||
unsafe {
|
||||
region::protect(m.as_mut_ptr(), m.len(), region::Protection::ReadExecute)
|
||||
}
|
||||
.expect("unable to make memory readonly and executable");
|
||||
}
|
||||
|
||||
t.publish(m.as_ptr() as u64)
|
||||
.expect("failed to publish function table");
|
||||
}
|
||||
|
||||
self.published = self.mmaps.len();
|
||||
}
|
||||
|
||||
/// Allocate `size` bytes of memory which can be made executable later by
|
||||
/// calling `publish()`. Note that we allocate the memory as writeable so
|
||||
/// that it can be written to and patched, though we make it readonly before
|
||||
/// actually executing from it.
|
||||
///
|
||||
/// TODO: Add an alignment flag.
|
||||
fn allocate(&mut self, size: usize) -> Result<(&mut [u8], &mut FunctionTable), String> {
|
||||
if self.current.0.len() - self.position < size {
|
||||
self.push_current(cmp::max(0x10000, size))?;
|
||||
}
|
||||
|
||||
let old_position = self.position;
|
||||
self.position += size;
|
||||
|
||||
Ok((
|
||||
&mut self.current.0.as_mut_slice()[old_position..self.position],
|
||||
&mut self.current.1,
|
||||
))
|
||||
}
|
||||
|
||||
/// Calculates the allocation size of the given compiled function.
|
||||
fn function_allocation_size(func: &CompiledFunction) -> usize {
|
||||
if func.unwind_info.is_empty() {
|
||||
func.body.len()
|
||||
} else {
|
||||
// Account for necessary unwind information alignment padding (32-bit)
|
||||
((func.body.len() + 3) & !3) + func.unwind_info.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// Copies the data of the compiled function to the given buffer.
|
||||
///
|
||||
/// This will also add the function to the current function table.
|
||||
fn copy_function<'a>(
|
||||
func: &CompiledFunction,
|
||||
func_start: u32,
|
||||
buf: &'a mut [u8],
|
||||
table: &'a mut FunctionTable,
|
||||
) -> (
|
||||
u32,
|
||||
&'a mut [u8],
|
||||
&'a mut FunctionTable,
|
||||
&'a mut [VMFunctionBody],
|
||||
) {
|
||||
let func_end = func_start + (func.body.len() as u32);
|
||||
|
||||
let (body, remainder) = buf.split_at_mut(func.body.len());
|
||||
body.copy_from_slice(&func.body);
|
||||
let vmfunc = Self::view_as_mut_vmfunc_slice(body);
|
||||
|
||||
if func.unwind_info.is_empty() {
|
||||
return (func_end, remainder, table, vmfunc);
|
||||
}
|
||||
|
||||
// Keep unwind information 32-bit aligned (round up to the nearest 4 byte boundary)
|
||||
let padding = ((func.body.len() + 3) & !3) - func.body.len();
|
||||
let (unwind, remainder) = remainder.split_at_mut(padding + func.unwind_info.len());
|
||||
unwind[padding..].copy_from_slice(&func.unwind_info);
|
||||
|
||||
let unwind_start = func_end + (padding as u32);
|
||||
let unwind_end = unwind_start + (func.unwind_info.len() as u32);
|
||||
|
||||
table.add_function(func_start, func_end, unwind_start);
|
||||
|
||||
(unwind_end, remainder, table, vmfunc)
|
||||
}
|
||||
|
||||
/// Convert mut a slice from u8 to VMFunctionBody.
|
||||
fn view_as_mut_vmfunc_slice(slice: &mut [u8]) -> &mut [VMFunctionBody] {
|
||||
let byte_ptr: *mut [u8] = slice;
|
||||
let body_ptr = byte_ptr as *mut [VMFunctionBody];
|
||||
unsafe { &mut *body_ptr }
|
||||
}
|
||||
|
||||
/// Pushes the current Mmap (and function table) and allocates a new Mmap of the given size.
|
||||
fn push_current(&mut self, new_size: usize) -> Result<(), String> {
|
||||
let previous = mem::replace(
|
||||
&mut self.current,
|
||||
(
|
||||
if new_size == 0 {
|
||||
Mmap::new()
|
||||
} else {
|
||||
Mmap::with_at_least(cmp::max(0x10000, new_size))?
|
||||
},
|
||||
FunctionTable::new(),
|
||||
),
|
||||
);
|
||||
|
||||
if previous.0.len() > 0 {
|
||||
self.mmaps.push(previous);
|
||||
} else {
|
||||
assert!(previous.1.len() == 0);
|
||||
}
|
||||
|
||||
self.position = 0;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
424
crates/jit/src/compiler.rs
Normal file
424
crates/jit/src/compiler.rs
Normal file
@@ -0,0 +1,424 @@
|
||||
//! JIT compilation.
|
||||
|
||||
use super::HashMap;
|
||||
use crate::code_memory::CodeMemory;
|
||||
use crate::instantiate::SetupError;
|
||||
use crate::target_tunables::target_tunables;
|
||||
use alloc::boxed::Box;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use core::convert::TryFrom;
|
||||
use cranelift_codegen::ir::InstBuilder;
|
||||
use cranelift_codegen::isa::{TargetFrontendConfig, TargetIsa};
|
||||
use cranelift_codegen::Context;
|
||||
use cranelift_codegen::{binemit, ir};
|
||||
use cranelift_entity::{EntityRef, PrimaryMap};
|
||||
use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext};
|
||||
use cranelift_wasm::{DefinedFuncIndex, DefinedMemoryIndex, ModuleTranslationState};
|
||||
use wasmtime_debug::{emit_debugsections_image, DebugInfoData};
|
||||
use wasmtime_environ::{
|
||||
Compilation, CompileError, CompiledFunction, Compiler as _C, FunctionBodyData, Module,
|
||||
ModuleVmctxInfo, Relocations, Traps, Tunables, VMOffsets,
|
||||
};
|
||||
use wasmtime_runtime::{
|
||||
get_mut_trap_registry, InstantiationError, SignatureRegistry, TrapRegistrationGuard,
|
||||
VMFunctionBody,
|
||||
};
|
||||
|
||||
/// Select which kind of compilation to use.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum CompilationStrategy {
|
||||
/// Let Wasmtime pick the strategy.
|
||||
Auto,
|
||||
|
||||
/// Compile all functions with Cranelift.
|
||||
Cranelift,
|
||||
|
||||
/// Compile all functions with Lightbeam.
|
||||
#[cfg(feature = "lightbeam")]
|
||||
Lightbeam,
|
||||
}
|
||||
|
||||
/// A WebAssembly code JIT compiler.
|
||||
///
|
||||
/// A `Compiler` instance owns the executable memory that it allocates.
|
||||
///
|
||||
/// TODO: Evolve this to support streaming rather than requiring a `&[u8]`
|
||||
/// containing a whole wasm module at once.
|
||||
///
|
||||
/// TODO: Consider using cranelift-module.
|
||||
pub struct Compiler {
|
||||
isa: Box<dyn TargetIsa>,
|
||||
|
||||
code_memory: CodeMemory,
|
||||
trap_registration_guards: Vec<TrapRegistrationGuard>,
|
||||
trampoline_park: HashMap<*const VMFunctionBody, *const VMFunctionBody>,
|
||||
signatures: SignatureRegistry,
|
||||
strategy: CompilationStrategy,
|
||||
|
||||
/// The `FunctionBuilderContext`, shared between trampline function compilations.
|
||||
fn_builder_ctx: FunctionBuilderContext,
|
||||
}
|
||||
|
||||
impl Compiler {
|
||||
/// Construct a new `Compiler`.
|
||||
pub fn new(isa: Box<dyn TargetIsa>, strategy: CompilationStrategy) -> Self {
|
||||
Self {
|
||||
isa,
|
||||
code_memory: CodeMemory::new(),
|
||||
trap_registration_guards: Vec::new(),
|
||||
trampoline_park: HashMap::new(),
|
||||
signatures: SignatureRegistry::new(),
|
||||
fn_builder_ctx: FunctionBuilderContext::new(),
|
||||
strategy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Compiler {
|
||||
fn drop(&mut self) {
|
||||
// We must deregister traps before freeing the code memory.
|
||||
// Otherwise, we have a race:
|
||||
// - Compiler #1 dropped code memory, but hasn't deregistered the trap yet
|
||||
// - Compiler #2 allocated code memory and tries to register a trap,
|
||||
// but the trap at certain address happens to be already registered,
|
||||
// since Compiler #1 hasn't deregistered it yet => assertion in trap registry fails.
|
||||
// Having a custom drop implementation we are independent from the field order
|
||||
// in the struct what reduces potential human error.
|
||||
self.trap_registration_guards.clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl Compiler {
|
||||
/// Return the target's frontend configuration settings.
|
||||
pub fn frontend_config(&self) -> TargetFrontendConfig {
|
||||
self.isa.frontend_config()
|
||||
}
|
||||
|
||||
/// Return the tunables in use by this engine.
|
||||
pub fn tunables(&self) -> Tunables {
|
||||
target_tunables(self.isa.triple())
|
||||
}
|
||||
|
||||
/// Compile the given function bodies.
|
||||
pub(crate) fn compile<'data>(
|
||||
&mut self,
|
||||
module: &Module,
|
||||
module_translation: &ModuleTranslationState,
|
||||
function_body_inputs: PrimaryMap<DefinedFuncIndex, FunctionBodyData<'data>>,
|
||||
debug_data: Option<DebugInfoData>,
|
||||
) -> Result<
|
||||
(
|
||||
PrimaryMap<DefinedFuncIndex, *mut [VMFunctionBody]>,
|
||||
PrimaryMap<DefinedFuncIndex, ir::JumpTableOffsets>,
|
||||
Relocations,
|
||||
Option<Vec<u8>>,
|
||||
),
|
||||
SetupError,
|
||||
> {
|
||||
let (compilation, relocations, address_transform, value_ranges, stack_slots, traps) =
|
||||
match self.strategy {
|
||||
// For now, interpret `Auto` as `Cranelift` since that's the most stable
|
||||
// implementation.
|
||||
CompilationStrategy::Auto | CompilationStrategy::Cranelift => {
|
||||
wasmtime_environ::cranelift::Cranelift::compile_module(
|
||||
module,
|
||||
module_translation,
|
||||
function_body_inputs,
|
||||
&*self.isa,
|
||||
debug_data.is_some(),
|
||||
)
|
||||
}
|
||||
#[cfg(feature = "lightbeam")]
|
||||
CompilationStrategy::Lightbeam => {
|
||||
wasmtime_environ::lightbeam::Lightbeam::compile_module(
|
||||
module,
|
||||
module_translation,
|
||||
function_body_inputs,
|
||||
&*self.isa,
|
||||
debug_data.is_some(),
|
||||
)
|
||||
}
|
||||
}
|
||||
.map_err(SetupError::Compile)?;
|
||||
|
||||
let allocated_functions =
|
||||
allocate_functions(&mut self.code_memory, &compilation).map_err(|message| {
|
||||
SetupError::Instantiate(InstantiationError::Resource(format!(
|
||||
"failed to allocate memory for functions: {}",
|
||||
message
|
||||
)))
|
||||
})?;
|
||||
|
||||
register_traps(
|
||||
&allocated_functions,
|
||||
&traps,
|
||||
&mut self.trap_registration_guards,
|
||||
);
|
||||
|
||||
let dbg = if let Some(debug_data) = debug_data {
|
||||
let target_config = self.isa.frontend_config();
|
||||
let triple = self.isa.triple().clone();
|
||||
let mut funcs = Vec::new();
|
||||
for (i, allocated) in allocated_functions.into_iter() {
|
||||
let ptr = (*allocated) as *const u8;
|
||||
let body_len = compilation.get(i).body.len();
|
||||
funcs.push((ptr, body_len));
|
||||
}
|
||||
let module_vmctx_info = {
|
||||
let ofs = VMOffsets::new(target_config.pointer_bytes(), &module);
|
||||
let memory_offset =
|
||||
ofs.vmctx_vmmemory_definition_base(DefinedMemoryIndex::new(0)) as i64;
|
||||
ModuleVmctxInfo {
|
||||
memory_offset,
|
||||
stack_slots,
|
||||
}
|
||||
};
|
||||
let bytes = emit_debugsections_image(
|
||||
triple,
|
||||
&target_config,
|
||||
&debug_data,
|
||||
&module_vmctx_info,
|
||||
&address_transform,
|
||||
&value_ranges,
|
||||
&funcs,
|
||||
)
|
||||
.map_err(|e| SetupError::DebugInfo(e))?;
|
||||
Some(bytes)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let jt_offsets = compilation.get_jt_offsets();
|
||||
|
||||
Ok((allocated_functions, jt_offsets, relocations, dbg))
|
||||
}
|
||||
|
||||
/// Create a trampoline for invoking a function.
|
||||
pub(crate) fn get_trampoline(
|
||||
&mut self,
|
||||
callee_address: *const VMFunctionBody,
|
||||
signature: &ir::Signature,
|
||||
value_size: usize,
|
||||
) -> Result<*const VMFunctionBody, SetupError> {
|
||||
use super::hash_map::Entry::{Occupied, Vacant};
|
||||
Ok(match self.trampoline_park.entry(callee_address) {
|
||||
Occupied(entry) => *entry.get(),
|
||||
Vacant(entry) => {
|
||||
let body = make_trampoline(
|
||||
&*self.isa,
|
||||
&mut self.code_memory,
|
||||
&mut self.fn_builder_ctx,
|
||||
callee_address,
|
||||
signature,
|
||||
value_size,
|
||||
)?;
|
||||
entry.insert(body);
|
||||
body
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Create and publish a trampoline for invoking a function.
|
||||
pub fn get_published_trampoline(
|
||||
&mut self,
|
||||
callee_address: *const VMFunctionBody,
|
||||
signature: &ir::Signature,
|
||||
value_size: usize,
|
||||
) -> Result<*const VMFunctionBody, SetupError> {
|
||||
let result = self.get_trampoline(callee_address, signature, value_size)?;
|
||||
self.publish_compiled_code();
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Make memory containing compiled code executable.
|
||||
pub(crate) fn publish_compiled_code(&mut self) {
|
||||
self.code_memory.publish();
|
||||
}
|
||||
|
||||
/// Shared signature registry.
|
||||
pub fn signatures(&mut self) -> &mut SignatureRegistry {
|
||||
&mut self.signatures
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a trampoline for invoking a function.
|
||||
fn make_trampoline(
|
||||
isa: &dyn TargetIsa,
|
||||
code_memory: &mut CodeMemory,
|
||||
fn_builder_ctx: &mut FunctionBuilderContext,
|
||||
callee_address: *const VMFunctionBody,
|
||||
signature: &ir::Signature,
|
||||
value_size: usize,
|
||||
) -> Result<*const VMFunctionBody, SetupError> {
|
||||
let pointer_type = isa.pointer_type();
|
||||
let mut wrapper_sig = ir::Signature::new(isa.frontend_config().default_call_conv);
|
||||
|
||||
// Add the `vmctx` parameter.
|
||||
wrapper_sig.params.push(ir::AbiParam::special(
|
||||
pointer_type,
|
||||
ir::ArgumentPurpose::VMContext,
|
||||
));
|
||||
// Add the `values_vec` parameter.
|
||||
wrapper_sig.params.push(ir::AbiParam::new(pointer_type));
|
||||
|
||||
let mut context = Context::new();
|
||||
context.func = ir::Function::with_name_signature(ir::ExternalName::user(0, 0), wrapper_sig);
|
||||
|
||||
{
|
||||
let mut builder = FunctionBuilder::new(&mut context.func, fn_builder_ctx);
|
||||
let block0 = builder.create_ebb();
|
||||
|
||||
builder.append_ebb_params_for_function_params(block0);
|
||||
builder.switch_to_block(block0);
|
||||
builder.seal_block(block0);
|
||||
|
||||
let (vmctx_ptr_val, values_vec_ptr_val) = {
|
||||
let params = builder.func.dfg.ebb_params(block0);
|
||||
(params[0], params[1])
|
||||
};
|
||||
|
||||
// Load the argument values out of `values_vec`.
|
||||
let mflags = ir::MemFlags::trusted();
|
||||
let callee_args = signature
|
||||
.params
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, r)| {
|
||||
match r.purpose {
|
||||
// i - 1 because vmctx isn't passed through `values_vec`.
|
||||
ir::ArgumentPurpose::Normal => builder.ins().load(
|
||||
r.value_type,
|
||||
mflags,
|
||||
values_vec_ptr_val,
|
||||
((i - 1) * value_size) as i32,
|
||||
),
|
||||
ir::ArgumentPurpose::VMContext => vmctx_ptr_val,
|
||||
other => panic!("unsupported argument purpose {}", other),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let new_sig = builder.import_signature(signature.clone());
|
||||
|
||||
// TODO: It's possible to make this a direct call. We just need Cranelift
|
||||
// to support functions declared with an immediate integer address.
|
||||
// ExternalName::Absolute(u64). Let's do it.
|
||||
let callee_value = builder.ins().iconst(pointer_type, callee_address as i64);
|
||||
let call = builder
|
||||
.ins()
|
||||
.call_indirect(new_sig, callee_value, &callee_args);
|
||||
|
||||
let results = builder.func.dfg.inst_results(call).to_vec();
|
||||
|
||||
// Store the return values into `values_vec`.
|
||||
let mflags = ir::MemFlags::trusted();
|
||||
for (i, r) in results.iter().enumerate() {
|
||||
builder
|
||||
.ins()
|
||||
.store(mflags, *r, values_vec_ptr_val, (i * value_size) as i32);
|
||||
}
|
||||
|
||||
builder.ins().return_(&[]);
|
||||
builder.finalize()
|
||||
}
|
||||
|
||||
let mut code_buf = Vec::new();
|
||||
let mut unwind_info = Vec::new();
|
||||
let mut reloc_sink = RelocSink {};
|
||||
let mut trap_sink = binemit::NullTrapSink {};
|
||||
let mut stackmap_sink = binemit::NullStackmapSink {};
|
||||
context
|
||||
.compile_and_emit(
|
||||
isa,
|
||||
&mut code_buf,
|
||||
&mut reloc_sink,
|
||||
&mut trap_sink,
|
||||
&mut stackmap_sink,
|
||||
)
|
||||
.map_err(|error| SetupError::Compile(CompileError::Codegen(error)))?;
|
||||
|
||||
context.emit_unwind_info(isa, &mut unwind_info);
|
||||
|
||||
Ok(code_memory
|
||||
.allocate_for_function(&CompiledFunction {
|
||||
body: code_buf,
|
||||
jt_offsets: context.func.jt_offsets,
|
||||
unwind_info,
|
||||
})
|
||||
.map_err(|message| SetupError::Instantiate(InstantiationError::Resource(message)))?
|
||||
.as_ptr())
|
||||
}
|
||||
|
||||
fn allocate_functions(
|
||||
code_memory: &mut CodeMemory,
|
||||
compilation: &Compilation,
|
||||
) -> Result<PrimaryMap<DefinedFuncIndex, *mut [VMFunctionBody]>, String> {
|
||||
let fat_ptrs = code_memory.allocate_for_compilation(compilation)?;
|
||||
|
||||
// Second, create a PrimaryMap from result vector of pointers.
|
||||
let mut result = PrimaryMap::with_capacity(compilation.len());
|
||||
for i in 0..fat_ptrs.len() {
|
||||
let fat_ptr: *mut [VMFunctionBody] = fat_ptrs[i];
|
||||
result.push(fat_ptr);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn register_traps(
|
||||
allocated_functions: &PrimaryMap<DefinedFuncIndex, *mut [VMFunctionBody]>,
|
||||
traps: &Traps,
|
||||
trap_registration_guards: &mut Vec<TrapRegistrationGuard>,
|
||||
) {
|
||||
let mut trap_registry = get_mut_trap_registry();
|
||||
for (func_addr, func_traps) in allocated_functions.values().zip(traps.values()) {
|
||||
for trap_desc in func_traps.iter() {
|
||||
let func_addr = *func_addr as *const u8 as usize;
|
||||
let offset = usize::try_from(trap_desc.code_offset).unwrap();
|
||||
let trap_addr = func_addr + offset;
|
||||
let guard =
|
||||
trap_registry.register_trap(trap_addr, trap_desc.source_loc, trap_desc.trap_code);
|
||||
trap_registration_guards.push(guard);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// We don't expect trampoline compilation to produce any relocations, so
|
||||
/// this `RelocSink` just asserts that it doesn't recieve any.
|
||||
struct RelocSink {}
|
||||
|
||||
impl binemit::RelocSink for RelocSink {
|
||||
fn reloc_ebb(
|
||||
&mut self,
|
||||
_offset: binemit::CodeOffset,
|
||||
_reloc: binemit::Reloc,
|
||||
_ebb_offset: binemit::CodeOffset,
|
||||
) {
|
||||
panic!("trampoline compilation should not produce ebb relocs");
|
||||
}
|
||||
fn reloc_external(
|
||||
&mut self,
|
||||
_offset: binemit::CodeOffset,
|
||||
_reloc: binemit::Reloc,
|
||||
_name: &ir::ExternalName,
|
||||
_addend: binemit::Addend,
|
||||
) {
|
||||
panic!("trampoline compilation should not produce external symbol relocs");
|
||||
}
|
||||
fn reloc_constant(
|
||||
&mut self,
|
||||
_code_offset: binemit::CodeOffset,
|
||||
_reloc: binemit::Reloc,
|
||||
_constant_offset: ir::ConstantOffset,
|
||||
) {
|
||||
panic!("trampoline compilation should not produce constant relocs");
|
||||
}
|
||||
fn reloc_jt(
|
||||
&mut self,
|
||||
_offset: binemit::CodeOffset,
|
||||
_reloc: binemit::Reloc,
|
||||
_jt: ir::JumpTable,
|
||||
) {
|
||||
panic!("trampoline compilation should not produce jump table relocs");
|
||||
}
|
||||
}
|
||||
251
crates/jit/src/context.rs
Normal file
251
crates/jit/src/context.rs
Normal file
@@ -0,0 +1,251 @@
|
||||
use crate::action::{get, inspect_memory, invoke};
|
||||
use crate::HashMap;
|
||||
use crate::{
|
||||
instantiate, ActionError, ActionOutcome, CompilationStrategy, CompiledModule, Compiler,
|
||||
InstanceHandle, Namespace, RuntimeValue, SetupError,
|
||||
};
|
||||
use alloc::boxed::Box;
|
||||
use alloc::rc::Rc;
|
||||
use alloc::string::{String, ToString};
|
||||
use core::cell::RefCell;
|
||||
use cranelift_codegen::isa::TargetIsa;
|
||||
use thiserror::Error;
|
||||
use wasmparser::{validate, OperatorValidatorConfig, ValidatingParserConfig};
|
||||
|
||||
/// Indicates an unknown instance was specified.
|
||||
#[derive(Error, Debug)]
|
||||
#[error("no instance {instance_name} present")]
|
||||
pub struct UnknownInstance {
|
||||
instance_name: String,
|
||||
}
|
||||
|
||||
/// Error message used by `WastContext`.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ContextError {
|
||||
/// An unknown instance name was used.
|
||||
#[error("{0}")]
|
||||
Instance(#[from] UnknownInstance),
|
||||
/// An error occured while performing an action.
|
||||
#[error("{0}")]
|
||||
Action(#[from] ActionError),
|
||||
}
|
||||
|
||||
/// The collection of features configurable during compilation
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Features {
|
||||
/// marks whether the proposed thread feature is enabled or disabled
|
||||
pub threads: bool,
|
||||
/// marks whether the proposed reference type feature is enabled or disabled
|
||||
pub reference_types: bool,
|
||||
/// marks whether the proposed SIMD feature is enabled or disabled
|
||||
pub simd: bool,
|
||||
/// marks whether the proposed bulk memory feature is enabled or disabled
|
||||
pub bulk_memory: bool,
|
||||
/// marks whether the proposed multi-value feature is enabled or disabled
|
||||
pub multi_value: bool,
|
||||
}
|
||||
|
||||
impl Into<ValidatingParserConfig> for Features {
|
||||
fn into(self) -> ValidatingParserConfig {
|
||||
ValidatingParserConfig {
|
||||
operator_config: OperatorValidatorConfig {
|
||||
enable_threads: self.threads,
|
||||
enable_reference_types: self.reference_types,
|
||||
enable_bulk_memory: self.bulk_memory,
|
||||
enable_simd: self.simd,
|
||||
enable_multi_value: self.multi_value,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A convenient context for compiling and executing WebAssembly instances.
|
||||
pub struct Context {
|
||||
namespace: Namespace,
|
||||
compiler: Box<Compiler>,
|
||||
global_exports: Rc<RefCell<HashMap<String, Option<wasmtime_runtime::Export>>>>,
|
||||
debug_info: bool,
|
||||
features: Features,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Construct a new instance of `Context`.
|
||||
pub fn new(compiler: Box<Compiler>) -> Self {
|
||||
Self {
|
||||
namespace: Namespace::new(),
|
||||
compiler,
|
||||
global_exports: Rc::new(RefCell::new(HashMap::new())),
|
||||
debug_info: false,
|
||||
features: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get debug_info settings.
|
||||
pub fn debug_info(&self) -> bool {
|
||||
self.debug_info
|
||||
}
|
||||
|
||||
/// Set debug_info settings.
|
||||
pub fn set_debug_info(&mut self, value: bool) {
|
||||
self.debug_info = value;
|
||||
}
|
||||
|
||||
/// Construct a new instance of `Context` with the given target.
|
||||
pub fn with_isa(isa: Box<dyn TargetIsa>, strategy: CompilationStrategy) -> Self {
|
||||
Self::new(Box::new(Compiler::new(isa, strategy)))
|
||||
}
|
||||
|
||||
/// Retrieve the context features
|
||||
pub fn features(&self) -> &Features {
|
||||
&self.features
|
||||
}
|
||||
|
||||
/// Construct a new instance with the given features from the current `Context`
|
||||
pub fn with_features(self, features: Features) -> Self {
|
||||
Self { features, ..self }
|
||||
}
|
||||
|
||||
fn validate(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
// TODO: Fix Cranelift to be able to perform validation itself, rather
|
||||
// than calling into wasmparser ourselves here.
|
||||
validate(data, Some(self.features.clone().into()))
|
||||
.map_err(|e| format!("module did not validate: {}", e.to_string()))
|
||||
}
|
||||
|
||||
fn instantiate(&mut self, data: &[u8]) -> Result<InstanceHandle, SetupError> {
|
||||
self.validate(&data).map_err(SetupError::Validate)?;
|
||||
let debug_info = self.debug_info();
|
||||
|
||||
instantiate(
|
||||
&mut *self.compiler,
|
||||
&data,
|
||||
&mut self.namespace,
|
||||
Rc::clone(&self.global_exports),
|
||||
debug_info,
|
||||
)
|
||||
}
|
||||
|
||||
/// Return the instance associated with the given name.
|
||||
pub fn get_instance(
|
||||
&mut self,
|
||||
instance_name: &str,
|
||||
) -> Result<&mut InstanceHandle, UnknownInstance> {
|
||||
self.namespace
|
||||
.get_instance(instance_name)
|
||||
.ok_or_else(|| UnknownInstance {
|
||||
instance_name: instance_name.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Instantiate a module instance and register the instance.
|
||||
pub fn instantiate_module(
|
||||
&mut self,
|
||||
instance_name: Option<String>,
|
||||
data: &[u8],
|
||||
) -> Result<InstanceHandle, ActionError> {
|
||||
let instance = self.instantiate(data).map_err(ActionError::Setup)?;
|
||||
self.optionally_name_instance(instance_name, instance.clone());
|
||||
Ok(instance)
|
||||
}
|
||||
|
||||
/// Compile a module.
|
||||
pub fn compile_module(&mut self, data: &[u8]) -> Result<CompiledModule, SetupError> {
|
||||
self.validate(&data).map_err(SetupError::Validate)?;
|
||||
let debug_info = self.debug_info();
|
||||
|
||||
CompiledModule::new(
|
||||
&mut *self.compiler,
|
||||
data,
|
||||
&mut self.namespace,
|
||||
Rc::clone(&self.global_exports),
|
||||
debug_info,
|
||||
)
|
||||
}
|
||||
|
||||
/// If `name` isn't None, register it for the given instance.
|
||||
pub fn optionally_name_instance(&mut self, name: Option<String>, instance: InstanceHandle) {
|
||||
if let Some(name) = name {
|
||||
self.namespace.name_instance(name, instance);
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a name for the given instance.
|
||||
pub fn name_instance(&mut self, name: String, instance: InstanceHandle) {
|
||||
self.namespace.name_instance(name, instance);
|
||||
}
|
||||
|
||||
/// Register an additional name for an existing registered instance.
|
||||
pub fn alias(&mut self, name: &str, as_name: String) -> Result<(), UnknownInstance> {
|
||||
let instance = self.get_instance(&name)?.clone();
|
||||
self.name_instance(as_name, instance);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Invoke an exported function from a named instance.
|
||||
pub fn invoke_named(
|
||||
&mut self,
|
||||
instance_name: &str,
|
||||
field: &str,
|
||||
args: &[RuntimeValue],
|
||||
) -> Result<ActionOutcome, ContextError> {
|
||||
let mut instance = self
|
||||
.get_instance(&instance_name)
|
||||
.map_err(ContextError::Instance)?
|
||||
.clone();
|
||||
self.invoke(&mut instance, field, args)
|
||||
.map_err(ContextError::Action)
|
||||
}
|
||||
|
||||
/// Invoke an exported function from an instance.
|
||||
pub fn invoke(
|
||||
&mut self,
|
||||
instance: &mut InstanceHandle,
|
||||
field: &str,
|
||||
args: &[RuntimeValue],
|
||||
) -> Result<ActionOutcome, ActionError> {
|
||||
invoke(&mut *self.compiler, instance, field, &args)
|
||||
}
|
||||
|
||||
/// Get the value of an exported global variable from an instance.
|
||||
pub fn get_named(
|
||||
&mut self,
|
||||
instance_name: &str,
|
||||
field: &str,
|
||||
) -> Result<ActionOutcome, ContextError> {
|
||||
let instance = self
|
||||
.get_instance(&instance_name)
|
||||
.map_err(ContextError::Instance)?
|
||||
.clone();
|
||||
self.get(&instance, field).map_err(ContextError::Action)
|
||||
}
|
||||
|
||||
/// Get the value of an exported global variable from an instance.
|
||||
pub fn get(
|
||||
&mut self,
|
||||
instance: &InstanceHandle,
|
||||
field: &str,
|
||||
) -> Result<ActionOutcome, ActionError> {
|
||||
get(instance, field).map(|value| ActionOutcome::Returned {
|
||||
values: vec![value],
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a slice of memory from an instance.
|
||||
pub fn inspect_memory<'instance>(
|
||||
&self,
|
||||
instance: &'instance InstanceHandle,
|
||||
field_name: &str,
|
||||
start: usize,
|
||||
len: usize,
|
||||
) -> Result<&'instance [u8], ActionError> {
|
||||
inspect_memory(instance, field_name, start, len)
|
||||
}
|
||||
|
||||
/// Return a handle to the global_exports mapping, needed by some modules
|
||||
/// for instantiation.
|
||||
pub fn get_global_exports(
|
||||
&mut self,
|
||||
) -> Rc<RefCell<HashMap<String, Option<wasmtime_runtime::Export>>>> {
|
||||
Rc::clone(&mut self.global_exports)
|
||||
}
|
||||
}
|
||||
134
crates/jit/src/function_table.rs
Normal file
134
crates/jit/src/function_table.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
//! Runtime function table.
|
||||
//!
|
||||
//! This module is primarily used to track JIT functions on Windows for stack walking and unwind.
|
||||
|
||||
/// Represents a runtime function table.
|
||||
///
|
||||
/// The runtime function table is not implemented for non-Windows target platforms.
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub(crate) struct FunctionTable;
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
impl FunctionTable {
|
||||
/// Creates a new function table.
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
/// Returns the number of functions in the table, also referred to as its 'length'.
|
||||
///
|
||||
/// For non-Windows platforms, the table will always be empty.
|
||||
pub fn len(&self) -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
/// Adds a function to the table based off of the start offset, end offset, and unwind offset.
|
||||
///
|
||||
/// The offsets are from the "module base", which is provided when the table is published.
|
||||
///
|
||||
/// For non-Windows platforms, this is a no-op.
|
||||
pub fn add_function(&mut self, _start: u32, _end: u32, _unwind: u32) {}
|
||||
|
||||
/// Publishes the function table using the given base address.
|
||||
///
|
||||
/// A published function table will automatically be deleted when it is dropped.
|
||||
///
|
||||
/// For non-Windows platforms, this is a no-op.
|
||||
pub fn publish(&mut self, _base_address: u64) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a runtime function table.
|
||||
///
|
||||
/// This is used to register JIT code with the operating system to enable stack walking and unwinding.
|
||||
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
|
||||
pub(crate) struct FunctionTable {
|
||||
functions: Vec<winapi::um::winnt::RUNTIME_FUNCTION>,
|
||||
published: bool,
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
|
||||
impl FunctionTable {
|
||||
/// Creates a new function table.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
functions: Vec::new(),
|
||||
published: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of functions in the table, also referred to as its 'length'.
|
||||
pub fn len(&self) -> usize {
|
||||
self.functions.len()
|
||||
}
|
||||
|
||||
/// Adds a function to the table based off of the start offset, end offset, and unwind offset.
|
||||
///
|
||||
/// The offsets are from the "module base", which is provided when the table is published.
|
||||
pub fn add_function(&mut self, start: u32, end: u32, unwind: u32) {
|
||||
use winapi::um::winnt;
|
||||
|
||||
assert!(!self.published, "table has already been published");
|
||||
|
||||
let mut entry = winnt::RUNTIME_FUNCTION::default();
|
||||
|
||||
entry.BeginAddress = start;
|
||||
entry.EndAddress = end;
|
||||
|
||||
unsafe {
|
||||
*entry.u.UnwindInfoAddress_mut() = unwind;
|
||||
}
|
||||
|
||||
self.functions.push(entry);
|
||||
}
|
||||
|
||||
/// Publishes the function table using the given base address.
|
||||
///
|
||||
/// A published function table will automatically be deleted when it is dropped.
|
||||
pub fn publish(&mut self, base_address: u64) -> Result<(), String> {
|
||||
use winapi::um::winnt;
|
||||
|
||||
if self.published {
|
||||
return Err("function table was already published".into());
|
||||
}
|
||||
|
||||
self.published = true;
|
||||
|
||||
if self.functions.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
unsafe {
|
||||
// Windows heap allocations are 32-bit aligned, but assert just in case
|
||||
assert!(
|
||||
(self.functions.as_mut_ptr() as u64) % 4 == 0,
|
||||
"function table allocation was not aligned"
|
||||
);
|
||||
|
||||
if winnt::RtlAddFunctionTable(
|
||||
self.functions.as_mut_ptr(),
|
||||
self.functions.len() as u32,
|
||||
base_address,
|
||||
) == 0
|
||||
{
|
||||
return Err("failed to add function table".into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
impl Drop for FunctionTable {
|
||||
fn drop(&mut self) {
|
||||
use winapi::um::winnt;
|
||||
|
||||
if self.published {
|
||||
unsafe {
|
||||
winnt::RtlDeleteFunctionTable(self.functions.as_mut_ptr());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
282
crates/jit/src/instantiate.rs
Normal file
282
crates/jit/src/instantiate.rs
Normal file
@@ -0,0 +1,282 @@
|
||||
//! Define the `instantiate` function, which takes a byte array containing an
|
||||
//! encoded wasm module and returns a live wasm instance. Also, define
|
||||
//! `CompiledModule` to allow compiling and instantiating to be done as separate
|
||||
//! steps.
|
||||
|
||||
use super::HashMap;
|
||||
use crate::compiler::Compiler;
|
||||
use crate::link::link_module;
|
||||
use crate::resolver::Resolver;
|
||||
use alloc::boxed::Box;
|
||||
use alloc::rc::Rc;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use core::cell::RefCell;
|
||||
use cranelift_entity::{BoxedSlice, PrimaryMap};
|
||||
use cranelift_wasm::{DefinedFuncIndex, SignatureIndex};
|
||||
#[cfg(feature = "std")]
|
||||
use std::io::Write;
|
||||
use thiserror::Error;
|
||||
use wasmtime_debug::read_debuginfo;
|
||||
use wasmtime_environ::{
|
||||
CompileError, DataInitializer, DataInitializerLocation, Module, ModuleEnvironment,
|
||||
};
|
||||
use wasmtime_runtime::{
|
||||
Export, GdbJitImageRegistration, Imports, InstanceHandle, InstantiationError, VMFunctionBody,
|
||||
VMSharedSignatureIndex,
|
||||
};
|
||||
|
||||
/// An error condition while setting up a wasm instance, be it validation,
|
||||
/// compilation, or instantiation.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum SetupError {
|
||||
/// The module did not pass validation.
|
||||
#[error("Validation error: {0}")]
|
||||
Validate(String),
|
||||
|
||||
/// A wasm translation error occured.
|
||||
#[error("WebAssembly compilation error: {0}")]
|
||||
Compile(#[from] CompileError),
|
||||
|
||||
/// Some runtime resource was unavailable or insufficient, or the start function
|
||||
/// trapped.
|
||||
#[error("Instantiation error: {0}")]
|
||||
Instantiate(#[from] InstantiationError),
|
||||
|
||||
/// Debug information generation error occured.
|
||||
#[error("Debug information error: {0}")]
|
||||
DebugInfo(failure::Error),
|
||||
}
|
||||
|
||||
/// This is similar to `CompiledModule`, but references the data initializers
|
||||
/// from the wasm buffer rather than holding its own copy.
|
||||
struct RawCompiledModule<'data> {
|
||||
module: Module,
|
||||
finished_functions: BoxedSlice<DefinedFuncIndex, *const VMFunctionBody>,
|
||||
imports: Imports,
|
||||
data_initializers: Box<[DataInitializer<'data>]>,
|
||||
signatures: BoxedSlice<SignatureIndex, VMSharedSignatureIndex>,
|
||||
dbg_jit_registration: Option<GdbJitImageRegistration>,
|
||||
}
|
||||
|
||||
impl<'data> RawCompiledModule<'data> {
|
||||
/// Create a new `RawCompiledModule` by compiling the wasm module in `data` and instatiating it.
|
||||
fn new(
|
||||
compiler: &mut Compiler,
|
||||
data: &'data [u8],
|
||||
resolver: &mut dyn Resolver,
|
||||
debug_info: bool,
|
||||
) -> Result<Self, SetupError> {
|
||||
let environ = ModuleEnvironment::new(compiler.frontend_config(), compiler.tunables());
|
||||
|
||||
let translation = environ
|
||||
.translate(data)
|
||||
.map_err(|error| SetupError::Compile(CompileError::Wasm(error)))?;
|
||||
|
||||
let debug_data = if debug_info {
|
||||
Some(read_debuginfo(&data))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (allocated_functions, jt_offsets, relocations, dbg_image) = compiler.compile(
|
||||
&translation.module,
|
||||
translation.module_translation.as_ref().unwrap(),
|
||||
translation.function_body_inputs,
|
||||
debug_data,
|
||||
)?;
|
||||
|
||||
let imports = link_module(
|
||||
&translation.module,
|
||||
&allocated_functions,
|
||||
&jt_offsets,
|
||||
relocations,
|
||||
resolver,
|
||||
)
|
||||
.map_err(|err| SetupError::Instantiate(InstantiationError::Link(err)))?;
|
||||
|
||||
// Gather up the pointers to the compiled functions.
|
||||
let finished_functions: BoxedSlice<DefinedFuncIndex, *const VMFunctionBody> =
|
||||
allocated_functions
|
||||
.into_iter()
|
||||
.map(|(_index, allocated)| {
|
||||
let fatptr: *const [VMFunctionBody] = *allocated;
|
||||
fatptr as *const VMFunctionBody
|
||||
})
|
||||
.collect::<PrimaryMap<_, _>>()
|
||||
.into_boxed_slice();
|
||||
|
||||
// Compute indices into the shared signature table.
|
||||
let signatures = {
|
||||
let signature_registry = compiler.signatures();
|
||||
translation
|
||||
.module
|
||||
.signatures
|
||||
.values()
|
||||
.map(|sig| signature_registry.register(sig))
|
||||
.collect::<PrimaryMap<_, _>>()
|
||||
};
|
||||
|
||||
// Make all code compiled thus far executable.
|
||||
compiler.publish_compiled_code();
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
let dbg_jit_registration = if let Some(img) = dbg_image {
|
||||
let mut bytes = Vec::new();
|
||||
bytes.write_all(&img).expect("all written");
|
||||
let reg = GdbJitImageRegistration::register(bytes);
|
||||
Some(reg)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
module: translation.module,
|
||||
finished_functions,
|
||||
imports,
|
||||
data_initializers: translation.data_initializers.into_boxed_slice(),
|
||||
signatures: signatures.into_boxed_slice(),
|
||||
dbg_jit_registration,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A compiled wasm module, ready to be instantiated.
|
||||
pub struct CompiledModule {
|
||||
module: Rc<Module>,
|
||||
finished_functions: BoxedSlice<DefinedFuncIndex, *const VMFunctionBody>,
|
||||
imports: Imports,
|
||||
data_initializers: Box<[OwnedDataInitializer]>,
|
||||
signatures: BoxedSlice<SignatureIndex, VMSharedSignatureIndex>,
|
||||
global_exports: Rc<RefCell<HashMap<String, Option<Export>>>>,
|
||||
dbg_jit_registration: Option<Rc<GdbJitImageRegistration>>,
|
||||
}
|
||||
|
||||
impl CompiledModule {
|
||||
/// Compile a data buffer into a `CompiledModule`, which may then be instantiated.
|
||||
pub fn new<'data>(
|
||||
compiler: &mut Compiler,
|
||||
data: &'data [u8],
|
||||
resolver: &mut dyn Resolver,
|
||||
global_exports: Rc<RefCell<HashMap<String, Option<Export>>>>,
|
||||
debug_info: bool,
|
||||
) -> Result<Self, SetupError> {
|
||||
let raw = RawCompiledModule::<'data>::new(compiler, data, resolver, debug_info)?;
|
||||
|
||||
Ok(Self::from_parts(
|
||||
raw.module,
|
||||
global_exports,
|
||||
raw.finished_functions,
|
||||
raw.imports,
|
||||
raw.data_initializers
|
||||
.iter()
|
||||
.map(OwnedDataInitializer::new)
|
||||
.collect::<Vec<_>>()
|
||||
.into_boxed_slice(),
|
||||
raw.signatures.clone(),
|
||||
raw.dbg_jit_registration,
|
||||
))
|
||||
}
|
||||
|
||||
/// Construct a `CompiledModule` from component parts.
|
||||
pub fn from_parts(
|
||||
module: Module,
|
||||
global_exports: Rc<RefCell<HashMap<String, Option<Export>>>>,
|
||||
finished_functions: BoxedSlice<DefinedFuncIndex, *const VMFunctionBody>,
|
||||
imports: Imports,
|
||||
data_initializers: Box<[OwnedDataInitializer]>,
|
||||
signatures: BoxedSlice<SignatureIndex, VMSharedSignatureIndex>,
|
||||
dbg_jit_registration: Option<GdbJitImageRegistration>,
|
||||
) -> Self {
|
||||
Self {
|
||||
module: Rc::new(module),
|
||||
global_exports: Rc::clone(&global_exports),
|
||||
finished_functions,
|
||||
imports,
|
||||
data_initializers,
|
||||
signatures,
|
||||
dbg_jit_registration: dbg_jit_registration.map(|r| Rc::new(r)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Crate an `Instance` from this `CompiledModule`.
|
||||
///
|
||||
/// Note that if only one instance of this module is needed, it may be more
|
||||
/// efficient to call the top-level `instantiate`, since that avoids copying
|
||||
/// the data initializers.
|
||||
pub fn instantiate(&mut self) -> Result<InstanceHandle, InstantiationError> {
|
||||
let data_initializers = self
|
||||
.data_initializers
|
||||
.iter()
|
||||
.map(|init| DataInitializer {
|
||||
location: init.location.clone(),
|
||||
data: &*init.data,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
InstanceHandle::new(
|
||||
Rc::clone(&self.module),
|
||||
Rc::clone(&self.global_exports),
|
||||
self.finished_functions.clone(),
|
||||
self.imports.clone(),
|
||||
&data_initializers,
|
||||
self.signatures.clone(),
|
||||
self.dbg_jit_registration.as_ref().map(|r| Rc::clone(&r)),
|
||||
Box::new(()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Return a reference-counting pointer to a module.
|
||||
pub fn module(&self) -> Rc<Module> {
|
||||
self.module.clone()
|
||||
}
|
||||
|
||||
/// Return a reference to a module.
|
||||
pub fn module_ref(&self) -> &Module {
|
||||
&self.module
|
||||
}
|
||||
}
|
||||
|
||||
/// Similar to `DataInitializer`, but owns its own copy of the data rather
|
||||
/// than holding a slice of the original module.
|
||||
pub struct OwnedDataInitializer {
|
||||
/// The location where the initialization is to be performed.
|
||||
location: DataInitializerLocation,
|
||||
|
||||
/// The initialization data.
|
||||
data: Box<[u8]>,
|
||||
}
|
||||
|
||||
impl OwnedDataInitializer {
|
||||
fn new(borrowed: &DataInitializer<'_>) -> Self {
|
||||
Self {
|
||||
location: borrowed.location.clone(),
|
||||
data: borrowed.data.to_vec().into_boxed_slice(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new wasm instance by compiling the wasm module in `data` and instatiating it.
|
||||
///
|
||||
/// This is equivalent to createing a `CompiledModule` and calling `instantiate()` on it,
|
||||
/// but avoids creating an intermediate copy of the data initializers.
|
||||
pub fn instantiate(
|
||||
compiler: &mut Compiler,
|
||||
data: &[u8],
|
||||
resolver: &mut dyn Resolver,
|
||||
global_exports: Rc<RefCell<HashMap<String, Option<Export>>>>,
|
||||
debug_info: bool,
|
||||
) -> Result<InstanceHandle, SetupError> {
|
||||
let raw = RawCompiledModule::new(compiler, data, resolver, debug_info)?;
|
||||
|
||||
InstanceHandle::new(
|
||||
Rc::new(raw.module),
|
||||
global_exports,
|
||||
raw.finished_functions,
|
||||
raw.imports,
|
||||
&*raw.data_initializers,
|
||||
raw.signatures,
|
||||
raw.dbg_jit_registration.map(|r| Rc::new(r)),
|
||||
Box::new(()),
|
||||
)
|
||||
.map_err(SetupError::Instantiate)
|
||||
}
|
||||
59
crates/jit/src/lib.rs
Normal file
59
crates/jit/src/lib.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
//! JIT-style runtime for WebAssembly using Cranelift.
|
||||
|
||||
#![deny(missing_docs, trivial_numeric_casts, unused_extern_crates)]
|
||||
#![warn(unused_import_braces)]
|
||||
#![cfg_attr(feature = "std", deny(unstable_features))]
|
||||
#![cfg_attr(feature = "clippy", plugin(clippy(conf_file = "../../clippy.toml")))]
|
||||
#![cfg_attr(
|
||||
feature = "cargo-clippy",
|
||||
allow(clippy::new_without_default, clippy::new_without_default_derive)
|
||||
)]
|
||||
#![cfg_attr(
|
||||
feature = "cargo-clippy",
|
||||
warn(
|
||||
clippy::float_arithmetic,
|
||||
clippy::mut_mut,
|
||||
clippy::nonminimal_bool,
|
||||
clippy::option_map_unwrap_or,
|
||||
clippy::option_map_unwrap_or_else,
|
||||
clippy::print_stdout,
|
||||
clippy::unicode_not_nfc,
|
||||
clippy::use_self
|
||||
)
|
||||
)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate alloc;
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
use hashbrown::{hash_map, HashMap, HashSet};
|
||||
#[cfg(feature = "std")]
|
||||
use std::collections::{hash_map, HashMap, HashSet};
|
||||
|
||||
mod action;
|
||||
mod code_memory;
|
||||
mod compiler;
|
||||
mod context;
|
||||
mod function_table;
|
||||
mod instantiate;
|
||||
mod link;
|
||||
mod namespace;
|
||||
mod resolver;
|
||||
mod target_tunables;
|
||||
|
||||
pub use crate::action::{ActionError, ActionOutcome, RuntimeValue};
|
||||
pub use crate::code_memory::CodeMemory;
|
||||
pub use crate::compiler::{CompilationStrategy, Compiler};
|
||||
pub use crate::context::{Context, ContextError, Features, UnknownInstance};
|
||||
pub use crate::instantiate::{instantiate, CompiledModule, SetupError};
|
||||
pub use crate::link::link_module;
|
||||
pub use crate::namespace::Namespace;
|
||||
pub use crate::resolver::{NullResolver, Resolver};
|
||||
pub use crate::target_tunables::target_tunables;
|
||||
|
||||
// Re-export `InstanceHandle` so that users won't need to separately depend on
|
||||
// wasmtime-runtime in common cases.
|
||||
pub use wasmtime_runtime::{InstanceHandle, InstantiationError};
|
||||
|
||||
/// Version number of this crate.
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
412
crates/jit/src/link.rs
Normal file
412
crates/jit/src/link.rs
Normal file
@@ -0,0 +1,412 @@
|
||||
//! Linking for JIT-compiled code.
|
||||
|
||||
use crate::resolver::Resolver;
|
||||
use crate::HashSet;
|
||||
use alloc::vec::Vec;
|
||||
use core::ptr::write_unaligned;
|
||||
use cranelift_codegen::binemit::Reloc;
|
||||
use cranelift_codegen::ir::JumpTableOffsets;
|
||||
use cranelift_entity::PrimaryMap;
|
||||
use cranelift_wasm::{DefinedFuncIndex, Global, GlobalInit, Memory, Table, TableElementType};
|
||||
use wasmtime_environ::{
|
||||
MemoryPlan, MemoryStyle, Module, Relocation, RelocationTarget, Relocations, TablePlan,
|
||||
};
|
||||
use wasmtime_runtime::libcalls;
|
||||
use wasmtime_runtime::{
|
||||
Export, Imports, InstanceHandle, LinkError, VMFunctionBody, VMFunctionImport, VMGlobalImport,
|
||||
VMMemoryImport, VMTableImport,
|
||||
};
|
||||
|
||||
/// Links a module that has been compiled with `compiled_module` in `wasmtime-environ`.
|
||||
pub fn link_module(
|
||||
module: &Module,
|
||||
allocated_functions: &PrimaryMap<DefinedFuncIndex, *mut [VMFunctionBody]>,
|
||||
jt_offsets: &PrimaryMap<DefinedFuncIndex, JumpTableOffsets>,
|
||||
relocations: Relocations,
|
||||
resolver: &mut dyn Resolver,
|
||||
) -> Result<Imports, LinkError> {
|
||||
let mut dependencies = HashSet::new();
|
||||
|
||||
let mut function_imports = PrimaryMap::with_capacity(module.imported_funcs.len());
|
||||
for (index, (ref module_name, ref field)) in module.imported_funcs.iter() {
|
||||
match resolver.resolve(module_name, field) {
|
||||
Some(export_value) => match export_value {
|
||||
Export::Function {
|
||||
address,
|
||||
signature,
|
||||
vmctx,
|
||||
} => {
|
||||
let import_signature = &module.signatures[module.functions[index]];
|
||||
if signature != *import_signature {
|
||||
// TODO: If the difference is in the calling convention,
|
||||
// we could emit a wrapper function to fix it up.
|
||||
return Err(LinkError(
|
||||
format!("{}/{}: incompatible import type: exported function with signature {} incompatible with function import with signature {}",
|
||||
module_name, field,
|
||||
signature, import_signature)
|
||||
));
|
||||
}
|
||||
dependencies.insert(unsafe { InstanceHandle::from_vmctx(vmctx) });
|
||||
function_imports.push(VMFunctionImport {
|
||||
body: address,
|
||||
vmctx,
|
||||
});
|
||||
}
|
||||
Export::Table { .. } | Export::Memory { .. } | Export::Global { .. } => {
|
||||
return Err(LinkError(format!(
|
||||
"{}/{}: incompatible import type: export incompatible with function import",
|
||||
module_name, field
|
||||
)));
|
||||
}
|
||||
},
|
||||
None => {
|
||||
return Err(LinkError(format!(
|
||||
"{}/{}: unknown import function: function not provided",
|
||||
module_name, field
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut table_imports = PrimaryMap::with_capacity(module.imported_tables.len());
|
||||
for (index, (ref module_name, ref field)) in module.imported_tables.iter() {
|
||||
match resolver.resolve(module_name, field) {
|
||||
Some(export_value) => match export_value {
|
||||
Export::Table {
|
||||
definition,
|
||||
vmctx,
|
||||
table,
|
||||
} => {
|
||||
let import_table = &module.table_plans[index];
|
||||
if !is_table_compatible(&table, import_table) {
|
||||
return Err(LinkError(format!(
|
||||
"{}/{}: incompatible import type: exported table incompatible with table import",
|
||||
module_name, field,
|
||||
)));
|
||||
}
|
||||
dependencies.insert(unsafe { InstanceHandle::from_vmctx(vmctx) });
|
||||
table_imports.push(VMTableImport {
|
||||
from: definition,
|
||||
vmctx,
|
||||
});
|
||||
}
|
||||
Export::Global { .. } | Export::Memory { .. } | Export::Function { .. } => {
|
||||
return Err(LinkError(format!(
|
||||
"{}/{}: incompatible import type: export incompatible with table import",
|
||||
module_name, field
|
||||
)));
|
||||
}
|
||||
},
|
||||
None => {
|
||||
return Err(LinkError(format!(
|
||||
"unknown import: no provided import table for {}/{}",
|
||||
module_name, field
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut memory_imports = PrimaryMap::with_capacity(module.imported_memories.len());
|
||||
for (index, (ref module_name, ref field)) in module.imported_memories.iter() {
|
||||
match resolver.resolve(module_name, field) {
|
||||
Some(export_value) => match export_value {
|
||||
Export::Memory {
|
||||
definition,
|
||||
vmctx,
|
||||
memory,
|
||||
} => {
|
||||
let import_memory = &module.memory_plans[index];
|
||||
if !is_memory_compatible(&memory, import_memory) {
|
||||
return Err(LinkError(format!(
|
||||
"{}/{}: incompatible import type: exported memory incompatible with memory import",
|
||||
module_name, field
|
||||
)));
|
||||
}
|
||||
|
||||
// Sanity-check: Ensure that the imported memory has at least
|
||||
// guard-page protections the importing module expects it to have.
|
||||
match (memory.style, &import_memory.style) {
|
||||
(
|
||||
MemoryStyle::Static { bound },
|
||||
MemoryStyle::Static {
|
||||
bound: import_bound,
|
||||
},
|
||||
) => {
|
||||
assert!(bound >= *import_bound);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
assert!(memory.offset_guard_size >= import_memory.offset_guard_size);
|
||||
|
||||
dependencies.insert(unsafe { InstanceHandle::from_vmctx(vmctx) });
|
||||
memory_imports.push(VMMemoryImport {
|
||||
from: definition,
|
||||
vmctx,
|
||||
});
|
||||
}
|
||||
Export::Table { .. } | Export::Global { .. } | Export::Function { .. } => {
|
||||
return Err(LinkError(format!(
|
||||
"{}/{}: incompatible import type: export incompatible with memory import",
|
||||
module_name, field
|
||||
)));
|
||||
}
|
||||
},
|
||||
None => {
|
||||
return Err(LinkError(format!(
|
||||
"unknown import: no provided import memory for {}/{}",
|
||||
module_name, field
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut global_imports = PrimaryMap::with_capacity(module.imported_globals.len());
|
||||
for (index, (ref module_name, ref field)) in module.imported_globals.iter() {
|
||||
match resolver.resolve(module_name, field) {
|
||||
Some(export_value) => match export_value {
|
||||
Export::Table { .. } | Export::Memory { .. } | Export::Function { .. } => {
|
||||
return Err(LinkError(format!(
|
||||
"{}/{}: incompatible import type: exported global incompatible with global import",
|
||||
module_name, field
|
||||
)));
|
||||
}
|
||||
Export::Global {
|
||||
definition,
|
||||
vmctx,
|
||||
global,
|
||||
} => {
|
||||
let imported_global = module.globals[index];
|
||||
if !is_global_compatible(&global, &imported_global) {
|
||||
return Err(LinkError(format!(
|
||||
"{}/{}: incompatible import type: exported global incompatible with global import",
|
||||
module_name, field
|
||||
)));
|
||||
}
|
||||
dependencies.insert(unsafe { InstanceHandle::from_vmctx(vmctx) });
|
||||
global_imports.push(VMGlobalImport { from: definition });
|
||||
}
|
||||
},
|
||||
None => {
|
||||
return Err(LinkError(format!(
|
||||
"unknown import: no provided import global for {}/{}",
|
||||
module_name, field
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply relocations, now that we have virtual addresses for everything.
|
||||
relocate(allocated_functions, jt_offsets, relocations, module);
|
||||
|
||||
Ok(Imports::new(
|
||||
dependencies,
|
||||
function_imports,
|
||||
table_imports,
|
||||
memory_imports,
|
||||
global_imports,
|
||||
))
|
||||
}
|
||||
|
||||
fn is_global_compatible(exported: &Global, imported: &Global) -> bool {
|
||||
match imported.initializer {
|
||||
GlobalInit::Import => (),
|
||||
_ => panic!("imported Global should have an Imported initializer"),
|
||||
}
|
||||
|
||||
let Global {
|
||||
ty: exported_ty,
|
||||
mutability: exported_mutability,
|
||||
initializer: _exported_initializer,
|
||||
} = exported;
|
||||
let Global {
|
||||
ty: imported_ty,
|
||||
mutability: imported_mutability,
|
||||
initializer: _imported_initializer,
|
||||
} = imported;
|
||||
exported_ty == imported_ty && imported_mutability == exported_mutability
|
||||
}
|
||||
|
||||
fn is_table_element_type_compatible(
|
||||
exported_type: TableElementType,
|
||||
imported_type: TableElementType,
|
||||
) -> bool {
|
||||
match exported_type {
|
||||
TableElementType::Func => match imported_type {
|
||||
TableElementType::Func => true,
|
||||
_ => false,
|
||||
},
|
||||
TableElementType::Val(exported_val_ty) => match imported_type {
|
||||
TableElementType::Val(imported_val_ty) => exported_val_ty == imported_val_ty,
|
||||
_ => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn is_table_compatible(exported: &TablePlan, imported: &TablePlan) -> bool {
|
||||
let TablePlan {
|
||||
table:
|
||||
Table {
|
||||
ty: exported_ty,
|
||||
minimum: exported_minimum,
|
||||
maximum: exported_maximum,
|
||||
},
|
||||
style: _exported_style,
|
||||
} = exported;
|
||||
let TablePlan {
|
||||
table:
|
||||
Table {
|
||||
ty: imported_ty,
|
||||
minimum: imported_minimum,
|
||||
maximum: imported_maximum,
|
||||
},
|
||||
style: _imported_style,
|
||||
} = imported;
|
||||
|
||||
is_table_element_type_compatible(*exported_ty, *imported_ty)
|
||||
&& imported_minimum <= exported_minimum
|
||||
&& (imported_maximum.is_none()
|
||||
|| (!exported_maximum.is_none()
|
||||
&& imported_maximum.unwrap() >= exported_maximum.unwrap()))
|
||||
}
|
||||
|
||||
fn is_memory_compatible(exported: &MemoryPlan, imported: &MemoryPlan) -> bool {
|
||||
let MemoryPlan {
|
||||
memory:
|
||||
Memory {
|
||||
minimum: exported_minimum,
|
||||
maximum: exported_maximum,
|
||||
shared: exported_shared,
|
||||
},
|
||||
style: _exported_style,
|
||||
offset_guard_size: _exported_offset_guard_size,
|
||||
} = exported;
|
||||
let MemoryPlan {
|
||||
memory:
|
||||
Memory {
|
||||
minimum: imported_minimum,
|
||||
maximum: imported_maximum,
|
||||
shared: imported_shared,
|
||||
},
|
||||
style: _imported_style,
|
||||
offset_guard_size: _imported_offset_guard_size,
|
||||
} = imported;
|
||||
|
||||
imported_minimum <= exported_minimum
|
||||
&& (imported_maximum.is_none()
|
||||
|| (!exported_maximum.is_none()
|
||||
&& imported_maximum.unwrap() >= exported_maximum.unwrap()))
|
||||
&& exported_shared == imported_shared
|
||||
}
|
||||
|
||||
/// Performs the relocations inside the function bytecode, provided the necessary metadata.
|
||||
fn relocate(
|
||||
allocated_functions: &PrimaryMap<DefinedFuncIndex, *mut [VMFunctionBody]>,
|
||||
jt_offsets: &PrimaryMap<DefinedFuncIndex, JumpTableOffsets>,
|
||||
relocations: PrimaryMap<DefinedFuncIndex, Vec<Relocation>>,
|
||||
module: &Module,
|
||||
) {
|
||||
for (i, function_relocs) in relocations.into_iter() {
|
||||
for r in function_relocs {
|
||||
use self::libcalls::*;
|
||||
let target_func_address: usize = match r.reloc_target {
|
||||
RelocationTarget::UserFunc(index) => match module.defined_func_index(index) {
|
||||
Some(f) => {
|
||||
let fatptr: *const [VMFunctionBody] = allocated_functions[f];
|
||||
fatptr as *const VMFunctionBody as usize
|
||||
}
|
||||
None => panic!("direct call to import"),
|
||||
},
|
||||
RelocationTarget::Memory32Grow => wasmtime_memory32_grow as usize,
|
||||
RelocationTarget::Memory32Size => wasmtime_memory32_size as usize,
|
||||
RelocationTarget::ImportedMemory32Grow => wasmtime_imported_memory32_grow as usize,
|
||||
RelocationTarget::ImportedMemory32Size => wasmtime_imported_memory32_size as usize,
|
||||
RelocationTarget::LibCall(libcall) => {
|
||||
use cranelift_codegen::ir::LibCall::*;
|
||||
match libcall {
|
||||
CeilF32 => wasmtime_f32_ceil as usize,
|
||||
FloorF32 => wasmtime_f32_floor as usize,
|
||||
TruncF32 => wasmtime_f32_trunc as usize,
|
||||
NearestF32 => wasmtime_f32_nearest as usize,
|
||||
CeilF64 => wasmtime_f64_ceil as usize,
|
||||
FloorF64 => wasmtime_f64_floor as usize,
|
||||
TruncF64 => wasmtime_f64_trunc as usize,
|
||||
NearestF64 => wasmtime_f64_nearest as usize,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
Probestack => __rust_probestack as usize,
|
||||
#[cfg(all(target_os = "windows", target_env = "gnu"))]
|
||||
Probestack => ___chkstk as usize,
|
||||
#[cfg(all(
|
||||
target_os = "windows",
|
||||
target_env = "msvc",
|
||||
target_pointer_width = "64"
|
||||
))]
|
||||
Probestack => __chkstk as usize,
|
||||
other => panic!("unexpected libcall: {}", other),
|
||||
}
|
||||
}
|
||||
RelocationTarget::JumpTable(func_index, jt) => {
|
||||
match module.defined_func_index(func_index) {
|
||||
Some(f) => {
|
||||
let offset = *jt_offsets
|
||||
.get(f)
|
||||
.and_then(|ofs| ofs.get(jt))
|
||||
.expect("func jump table");
|
||||
let fatptr: *const [VMFunctionBody] = allocated_functions[f];
|
||||
fatptr as *const VMFunctionBody as usize + offset as usize
|
||||
}
|
||||
None => panic!("func index of jump table"),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let fatptr: *const [VMFunctionBody] = allocated_functions[i];
|
||||
let body = fatptr as *const VMFunctionBody;
|
||||
match r.reloc {
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
Reloc::Abs8 => unsafe {
|
||||
let reloc_address = body.add(r.offset as usize) as usize;
|
||||
let reloc_addend = r.addend as isize;
|
||||
let reloc_abs = (target_func_address as u64)
|
||||
.checked_add(reloc_addend as u64)
|
||||
.unwrap();
|
||||
write_unaligned(reloc_address as *mut u64, reloc_abs);
|
||||
},
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
Reloc::X86PCRel4 => unsafe {
|
||||
let reloc_address = body.add(r.offset as usize) as usize;
|
||||
let reloc_addend = r.addend as isize;
|
||||
let reloc_delta_u32 = (target_func_address as u32)
|
||||
.wrapping_sub(reloc_address as u32)
|
||||
.checked_add(reloc_addend as u32)
|
||||
.unwrap();
|
||||
write_unaligned(reloc_address as *mut u32, reloc_delta_u32);
|
||||
},
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
Reloc::X86CallPCRel4 => {
|
||||
// ignore
|
||||
}
|
||||
Reloc::X86PCRelRodata4 => {
|
||||
// ignore
|
||||
}
|
||||
_ => panic!("unsupported reloc kind"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A declaration for the stack probe function in Rust's standard library, for
|
||||
/// catching callstack overflow.
|
||||
extern "C" {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn __rust_probestack();
|
||||
#[cfg(all(
|
||||
target_os = "windows",
|
||||
target_env = "msvc",
|
||||
target_pointer_width = "64"
|
||||
))]
|
||||
pub fn __chkstk();
|
||||
// ___chkstk (note the triple underscore) is implemented in compiler-builtins/src/x86_64.rs
|
||||
// by the Rust compiler for the MinGW target
|
||||
#[cfg(all(target_os = "windows", target_env = "gnu",))]
|
||||
pub fn ___chkstk();
|
||||
}
|
||||
47
crates/jit/src/namespace.rs
Normal file
47
crates/jit/src/namespace.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
//! The core WebAssembly spec does not specify how imports are to be resolved
|
||||
//! to exports. This file provides one possible way to manage multiple instances
|
||||
//! and resolve imports to exports among them.
|
||||
|
||||
use super::HashMap;
|
||||
use crate::resolver::Resolver;
|
||||
use alloc::string::String;
|
||||
use wasmtime_runtime::{Export, InstanceHandle};
|
||||
|
||||
/// A namespace containing instances keyed by name.
|
||||
///
|
||||
/// Note that `Namespace` implements the `Resolver` trait, so it can resolve
|
||||
/// imports using defined exports.
|
||||
pub struct Namespace {
|
||||
/// Mapping from identifiers to indices in `self.instances`.
|
||||
names: HashMap<String, InstanceHandle>,
|
||||
}
|
||||
|
||||
impl Namespace {
|
||||
/// Construct a new `Namespace`.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
names: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Install a new `InstanceHandle` in this `Namespace`, optionally with the
|
||||
/// given name.
|
||||
pub fn name_instance(&mut self, name: String, instance: InstanceHandle) {
|
||||
self.names.insert(name, instance);
|
||||
}
|
||||
|
||||
/// Get the instance registered with the given `instance_name`.
|
||||
pub fn get_instance(&mut self, name: &str) -> Option<&mut InstanceHandle> {
|
||||
self.names.get_mut(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolver for Namespace {
|
||||
fn resolve(&mut self, name: &str, field: &str) -> Option<Export> {
|
||||
if let Some(instance) = self.names.get_mut(name) {
|
||||
instance.lookup(field)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
19
crates/jit/src/resolver.rs
Normal file
19
crates/jit/src/resolver.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
//! Define the `Resolver` trait, allowing custom resolution for external
|
||||
//! references.
|
||||
|
||||
use wasmtime_runtime::Export;
|
||||
|
||||
/// Import resolver connects imports with available exported values.
|
||||
pub trait Resolver {
|
||||
/// Resolve the given module/field combo.
|
||||
fn resolve(&mut self, module: &str, field: &str) -> Option<Export>;
|
||||
}
|
||||
|
||||
/// `Resolver` implementation that always resolves to `None`.
|
||||
pub struct NullResolver {}
|
||||
|
||||
impl Resolver for NullResolver {
|
||||
fn resolve(&mut self, _module: &str, _field: &str) -> Option<Export> {
|
||||
None
|
||||
}
|
||||
}
|
||||
22
crates/jit/src/target_tunables.rs
Normal file
22
crates/jit/src/target_tunables.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use core::cmp::min;
|
||||
use target_lexicon::{OperatingSystem, Triple};
|
||||
use wasmtime_environ::Tunables;
|
||||
|
||||
/// Return a `Tunables` instance tuned for the given target platform.
|
||||
pub fn target_tunables(triple: &Triple) -> Tunables {
|
||||
let mut result = Tunables::default();
|
||||
|
||||
match triple.operating_system {
|
||||
OperatingSystem::Windows => {
|
||||
// For now, use a smaller footprint on Windows so that we don't
|
||||
// don't outstrip the paging file.
|
||||
// TODO: Make this configurable.
|
||||
result.static_memory_bound = min(result.static_memory_bound, 0x100);
|
||||
result.static_memory_offset_guard_size =
|
||||
min(result.static_memory_offset_guard_size, 0x10000);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user