diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index 286c62718b..cff2c49d81 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -69,7 +69,7 @@ jobs: displayName: Fetch cargo dependencies # Build and test all features except for lightbeam - - bash: cargo test --all --exclude lightbeam --exclude wasmtime-wasi-c + - bash: cargo test --all --exclude lightbeam --exclude wasmtime-wasi-c --exclude wasmtime-py displayName: Cargo test env: RUST_BACKTRACE: 1 @@ -117,34 +117,46 @@ jobs: image: centos:6 options: "--name ci-container -v /usr/bin/docker:/tmp/docker:ro" steps: - # We're executing in the container as non-root but `yum` requires root. We - # need to install `sudo` but to do that we need `sudo`. Do a bit of a weird - # hack where we use the host `docker` executable to re-execute in our own - # container with the root user to install `sudo` - - bash: /tmp/docker exec -t -u 0 ci-container sh -c "yum install -y sudo" - displayName: Configure sudo - - # See https://edwards.sdsu.edu/research/c11-on-centos-6/ for where these - # various commands came from. - - bash: | - set -e - sudo yum install -y centos-release-scl cmake xz - sudo yum install -y devtoolset-8-gcc devtoolset-8-binutils devtoolset-8-gcc-c++ - echo "##vso[task.prependpath]/opt/rh/devtoolset-8/root/usr/bin" - displayName: Install system dependencies - - # Delete `libstdc++.so` to force gcc to link against `libstdc++.a` instead. - # This is a hack and not the right way to do this, but it ends up doing the - # right thing for now. - - bash: sudo rm -f /opt/rh/devtoolset-8/root/usr/lib/gcc/x86_64-redhat-linux/8/libstdc++.so - displayName: Force a static libstdc++ - + - template: ci/azure-prepare-centos-6.yml - template: ci/azure-build-release.yml +# Build the `wasmtime-py` python extension in the same manner we build the +# binaries above, since these wheels are also native code that we're +# distributing. +# +# Note that the builds here are using a nightly compiler, not a stable compiler, +# since this is what PyO3 requires. +- job: Build_wheels + strategy: + matrix: + windows: + imageName: 'vs2017-win2016' + RUSTFLAGS: -Ctarget-feature=+crt-static + mac: + imageName: 'macos-10.14' + MACOSX_DEPLOYMENT_TARGET: 10.9 + variables: + toolchain: nightly-2019-08-15 + pool: + vmImage: $(imageName) + steps: + - template: ci/azure-build-wheels.yml +- job: Build_linux_wheels + variables: + toolchain: nightly-2019-08-15 + container: + image: centos:6 + options: "--name ci-container -v /usr/bin/docker:/tmp/docker:ro" + steps: + - template: ci/azure-prepare-centos-6.yml + - template: ci/azure-build-wheels.yml + - job: Publish dependsOn: - Build + - Build_wheels - Build_linux + - Build_linux_wheels condition: and(succeeded(), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI')) steps: # Checking out the sources is needed to be able to delete the "dev" tag, see below. diff --git a/Cargo.toml b/Cargo.toml index 86375aeef8..0a2cca5cf3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ cranelift-entity = { version = "0.38.0", features = ["enable-serde"] } cranelift-wasm = { version = "0.38.0", features = ["enable-serde"] } wasmtime-debug = { path = "wasmtime-debug" } wasmtime-environ = { path = "wasmtime-environ" } +wasmtime-interface-types = { path = "wasmtime-interface-types" } wasmtime-runtime = { path = "wasmtime-runtime" } wasmtime-jit = { path = "wasmtime-jit" } wasmtime-obj = { path = "wasmtime-obj" } @@ -26,6 +27,7 @@ wasi-common = { git = "https://github.com/CraneStation/wasi-common", rev = "8ea7 docopt = "1.0.1" serde = { "version" = "1.0.94", features = ["derive"] } faerie = "0.10.1" +failure = "0.1" target-lexicon = { version = "0.4.0", default-features = false } pretty_env_logger = "0.3.0" file-per-thread-logger = "0.1.1" @@ -33,8 +35,10 @@ wabt = "0.7" libc = "0.2.60" errno = "0.2.4" rayon = "1.1" +wasm-webidl-bindings = "0.4" [workspace] +members = ["misc/wasmtime-py"] [features] lightbeam = ["wasmtime-environ/lightbeam", "wasmtime-jit/lightbeam"] diff --git a/ci/azure-build-release.yml b/ci/azure-build-release.yml index a9f4513689..f76e02436b 100644 --- a/ci/azure-build-release.yml +++ b/ci/azure-build-release.yml @@ -36,7 +36,7 @@ steps: # Test what we're about to release in release mode itself. This tests # everything except lightbeam which requires nightly which happens above. -- bash: cargo test --release --all --exclude lightbeam --exclude wasmtime-wasi-c +- bash: cargo test --release --all --exclude lightbeam --exclude wasmtime-wasi-c --exclude wasmtime-py displayName: Cargo test env: RUST_BACKTRACE: 1 @@ -100,4 +100,3 @@ steps: inputs: path: $(Build.ArtifactStagingDirectory)/ artifactName: 'bundle-$(Agent.OS)' - diff --git a/ci/azure-build-wheels.yml b/ci/azure-build-wheels.yml new file mode 100644 index 0000000000..8d84a9ac5e --- /dev/null +++ b/ci/azure-build-wheels.yml @@ -0,0 +1,73 @@ +steps: +- checkout: self + submodules: true + +- template: azure-install-rust.yml + +- bash: mkdir misc/wasmtime-py/wheelhouse + displayName: Pre-create wheelhouse directory + +# Note that we skip this on Linux because Python 3.6 is pre-installed in the +# CentOS container. +- task: UsePythonVersion@0 + inputs: + versionSpec: '3.6' + addToPath: true + condition: and(succeeded(), ne(variables['Agent.OS'], 'Linux')) + +# Install Python dependencies needed for our `setup.py` scripts +- bash: sudo pip3 install setuptools wheel==0.31.1 setuptools-rust auditwheel + displayName: Install Python dependencies (Linux) + condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) +- bash: pip3 install setuptools wheel==0.31.1 setuptools-rust + displayName: Install Python dependencies (not Linux) + condition: and(succeeded(), ne(variables['Agent.OS'], 'Linux')) + +- bash: python setup.py bdist_wheel + workingDirectory: misc/wasmtime-py + displayName: Build wheels py36 + +# Clear the build directory between building different wheels for different +# Python versions to ensure that we don't package dynamic libraries twice by +# accident. +- bash: rm -rf build + workingDirectory: misc/wasmtime-py + displayName: Clear build directory + +# Note that 3.7 isn't installed on Linux so we don't do this a second time +# around. +- task: UsePythonVersion@0 + inputs: + versionSpec: '3.7' + addToPath: true + condition: and(succeeded(), ne(variables['Agent.OS'], 'Linux')) +- bash: | + set -e + pip3 install setuptools wheel==0.31.1 setuptools-rust + python setup.py bdist_wheel + workingDirectory: misc/wasmtime-py + displayName: Build wheels py37 + condition: and(succeeded(), ne(variables['Agent.OS'], 'Linux')) + +# Move `dist/*.whl` into `wheelhouse/` so we can deploy them, but on Linux we +# need to run an `auditwheel` command as well to turn these into "manylinux" +# wheels to run across a number of distributions. +- bash: mv dist/*.whl wheelhouse/ + workingDirectory: misc/wasmtime-py + displayName: Move wheels to wheelhouse (not Linux) + condition: and(succeeded(), ne(variables['Agent.OS'], 'Linux')) +- bash: | + set -e + for whl in dist/*.whl; do + auditwheel repair "$whl" -w wheelhouse/ + done + workingDirectory: misc/wasmtime-py + displayName: Move wheels to wheelhouse (Linux) + condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) + +# Publish our wheelhouse to azure pipelines which will later get published to +# github releases +- task: PublishPipelineArtifact@1 + inputs: + path: misc/wasmtime-py/wheelhouse + artifactName: 'wheels-$(Agent.OS)' diff --git a/ci/azure-prepare-centos-6.yml b/ci/azure-prepare-centos-6.yml new file mode 100644 index 0000000000..e5f85b544a --- /dev/null +++ b/ci/azure-prepare-centos-6.yml @@ -0,0 +1,24 @@ +steps: +# We're executing in the container as non-root but `yum` requires root. We +# need to install `sudo` but to do that we need `sudo`. Do a bit of a weird +# hack where we use the host `docker` executable to re-execute in our own +# container with the root user to install `sudo` +- bash: /tmp/docker exec -t -u 0 ci-container sh -c "yum install -y sudo" + displayName: Configure sudo + +# See https://edwards.sdsu.edu/research/c11-on-centos-6/ for where these +# various commands came from. +- bash: | + set -e + sudo yum install -y centos-release-scl cmake xz epel-release + sudo yum install -y rh-python36 patchelf unzip + sudo yum install -y devtoolset-8-gcc devtoolset-8-binutils devtoolset-8-gcc-c++ + echo "##vso[task.prependpath]/opt/rh/devtoolset-8/root/usr/bin" + echo "##vso[task.prependpath]/opt/rh/rh-python36/root/usr/bin" + displayName: Install system dependencies + +# Delete `libstdc++.so` to force gcc to link against `libstdc++.a` instead. +# This is a hack and not the right way to do this, but it ends up doing the +# right thing for now. +- bash: sudo rm -f /opt/rh/devtoolset-8/root/usr/lib/gcc/x86_64-redhat-linux/8/libstdc++.so + displayName: Force a static libstdc++ diff --git a/misc/wasmtime-py/.gitignore b/misc/wasmtime-py/.gitignore new file mode 100644 index 0000000000..cdee78184b --- /dev/null +++ b/misc/wasmtime-py/.gitignore @@ -0,0 +1,15 @@ +*.bk +*.swp +*.swo +*.swx +tags +target +Cargo.lock +.*.rustfmt +cranelift.dbg* +rusty-tags.* +*~ +\#*\# +build +dist +*.egg-info diff --git a/misc/wasmtime-py/Cargo.toml b/misc/wasmtime-py/Cargo.toml new file mode 100644 index 0000000000..58ca881057 --- /dev/null +++ b/misc/wasmtime-py/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "wasmtime-py" +version = "0.3.0" +authors = ["The Wasmtime Project Developers"] +description = "Python extension for the wasmtime" +license = "Apache-2.0 WITH LLVM-exception" +categories = ["wasm", "python"] +edition = "2018" +publish = false + +[lib] +name = "_wasmtime" +crate-type = ["cdylib"] + +[dependencies] +cranelift-codegen = "0.38.0" +cranelift-native = "0.38.0" +cranelift-entity = "0.38.0" +cranelift-wasm = "0.38.0" +cranelift-frontend = "0.38.0" +wasmtime-environ = { path = "../../wasmtime-environ" } +wasmtime-interface-types = { path = "../../wasmtime-interface-types" } +wasmtime-jit = { path = "../../wasmtime-jit" } +wasmtime-runtime = { path = "../../wasmtime-runtime" } +target-lexicon = { version = "0.4.0", default-features = false } +failure = "0.1" +region = "2.0.0" +wasmparser = "0.35.3" + +[dependencies.pyo3] +version = "0.7.0-alpha.1" +features = ["extension-module"] diff --git a/misc/wasmtime-py/LICENSE b/misc/wasmtime-py/LICENSE new file mode 100644 index 0000000000..f9d81955f4 --- /dev/null +++ b/misc/wasmtime-py/LICENSE @@ -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. + diff --git a/misc/wasmtime-py/README.md b/misc/wasmtime-py/README.md new file mode 100644 index 0000000000..4e38bd46fa --- /dev/null +++ b/misc/wasmtime-py/README.md @@ -0,0 +1,20 @@ +Python 3 extension for interface with Wasmtime/Cranelift. + +# Build + +First, you'll need to install some Python dependencies: + +``` +$ pip3 install setuptools wheel==0.31.1 setuptools-rust +``` + +Next you can build the extension with: + +``` +rustup run nightly python3 setup.py build +``` + +Note that a nightly version of Rust is required due to our usage of PyO3. + +This will create a directory called `build/lib` which you can add to +`PYTHONPATH` in order to get `import wasmtime` working. diff --git a/misc/wasmtime-py/examples/gcd/.gitignore b/misc/wasmtime-py/examples/gcd/.gitignore new file mode 100644 index 0000000000..46cc3441e8 --- /dev/null +++ b/misc/wasmtime-py/examples/gcd/.gitignore @@ -0,0 +1 @@ +gcd.wasm diff --git a/misc/wasmtime-py/examples/gcd/README.md b/misc/wasmtime-py/examples/gcd/README.md new file mode 100644 index 0000000000..f8be453d28 --- /dev/null +++ b/misc/wasmtime-py/examples/gcd/README.md @@ -0,0 +1,15 @@ +# Build example's file + +To build `gcd.wasm` use rustc (nightly) for wasm32 target with debug information: + +``` +rustc +nightly --target=wasm32-unknown-unknown -g gcd.rs --crate-type=cdylib +``` + +# Run example + +Point path to the built wasmtime_py library location when running python, e.g. + +``` +PYTHONPATH=../../target/debug python3 run.py +``` diff --git a/misc/wasmtime-py/examples/gcd/gcd.rs b/misc/wasmtime-py/examples/gcd/gcd.rs new file mode 100644 index 0000000000..fb2acb20b3 --- /dev/null +++ b/misc/wasmtime-py/examples/gcd/gcd.rs @@ -0,0 +1,19 @@ + +#[inline(never)] +#[no_mangle] +pub extern fn gcd(m_: u32, n_: u32) -> u32 +{ + let mut m = m_; + let mut n = n_; + while m > 0 { + let tmp = m; + m = n % m; + n = tmp; + } + return n; +} + +#[no_mangle] +pub extern fn test() -> u32 { + gcd(24, 9) +} diff --git a/misc/wasmtime-py/examples/gcd/run.py b/misc/wasmtime-py/examples/gcd/run.py new file mode 100644 index 0000000000..648d1ecc6d --- /dev/null +++ b/misc/wasmtime-py/examples/gcd/run.py @@ -0,0 +1,5 @@ +import wasmtime +import gcd + +print("gcd(27, 6) =", gcd.gcd(27, 6)) + diff --git a/misc/wasmtime-py/examples/import/.gitignore b/misc/wasmtime-py/examples/import/.gitignore new file mode 100644 index 0000000000..2a6475ec63 --- /dev/null +++ b/misc/wasmtime-py/examples/import/.gitignore @@ -0,0 +1,3 @@ +import.wasm +main.wasm +__pycache__ \ No newline at end of file diff --git a/misc/wasmtime-py/examples/import/README.md b/misc/wasmtime-py/examples/import/README.md new file mode 100644 index 0000000000..daa09b9b84 --- /dev/null +++ b/misc/wasmtime-py/examples/import/README.md @@ -0,0 +1,15 @@ +# Build example's file + +To build `main.wasm` use rustc (nightly) for wasm32 target with debug information: + +``` +rustc +nightly --target=wasm32-unknown-unknown main.rs --crate-type=cdylib +``` + +# Run example + +Point path to the built wasmtime_py library location when running python, e.g. + +``` +PYTHONPATH=../../target/debug python3 run.py +``` diff --git a/misc/wasmtime-py/examples/import/env.py b/misc/wasmtime-py/examples/import/env.py new file mode 100644 index 0000000000..83da582a16 --- /dev/null +++ b/misc/wasmtime-py/examples/import/env.py @@ -0,0 +1,10 @@ +def callback(msg_p: 'i32', msg_len: 'i32') -> 'i32': + print('callback:', msg_p, msg_len) + +# global memory +# mv = memoryview(memory) + +# msg = bytes(mv[msg_p:(msg_p + msg_len)]).decode('utf-8') +# print(msg) + + return 42 diff --git a/misc/wasmtime-py/examples/import/main.rs b/misc/wasmtime-py/examples/import/main.rs new file mode 100644 index 0000000000..cbe9cae958 --- /dev/null +++ b/misc/wasmtime-py/examples/import/main.rs @@ -0,0 +1,11 @@ +extern "C" { + fn callback(s: *const u8, s_len: u32) -> u32; +} + +#[no_mangle] +pub extern "C" fn test() { + let msg = "Hello, world!"; + unsafe { + callback(msg.as_ptr(), msg.len() as u32); + } +} diff --git a/misc/wasmtime-py/examples/import/run.py b/misc/wasmtime-py/examples/import/run.py new file mode 100644 index 0000000000..e190f2b7b7 --- /dev/null +++ b/misc/wasmtime-py/examples/import/run.py @@ -0,0 +1,4 @@ +import wasmtime +import main + +main.test() diff --git a/misc/wasmtime-py/examples/two_modules/.gitignore b/misc/wasmtime-py/examples/two_modules/.gitignore new file mode 100644 index 0000000000..64ee0b754f --- /dev/null +++ b/misc/wasmtime-py/examples/two_modules/.gitignore @@ -0,0 +1,3 @@ +one.wasm +two.wasm +__pycache__ \ No newline at end of file diff --git a/misc/wasmtime-py/examples/two_modules/README.md b/misc/wasmtime-py/examples/two_modules/README.md new file mode 100644 index 0000000000..e23101408b --- /dev/null +++ b/misc/wasmtime-py/examples/two_modules/README.md @@ -0,0 +1,20 @@ +# Build example's file + +To build `one.wasm` use rustc (nightly) for wasm32 target with debug information: + +``` +rustc +nightly --target=wasm32-unknown-unknown one.rs --crate-type=cdylib +``` + +To build `two.wasm` use wabt. +``` +wat2wasm two.wat -o two.wasm +``` + +# Run example + +Point path to the built wasmtime_py library location when running python, e.g. + +``` +PYTHONPATH=../../target/debug python3 run.py +``` diff --git a/misc/wasmtime-py/examples/two_modules/env.py b/misc/wasmtime-py/examples/two_modules/env.py new file mode 100644 index 0000000000..5af7f13e3f --- /dev/null +++ b/misc/wasmtime-py/examples/two_modules/env.py @@ -0,0 +1,2 @@ +def answer() -> 'i32': + return 42 diff --git a/misc/wasmtime-py/examples/two_modules/one.rs b/misc/wasmtime-py/examples/two_modules/one.rs new file mode 100644 index 0000000000..bbd51ec5f4 --- /dev/null +++ b/misc/wasmtime-py/examples/two_modules/one.rs @@ -0,0 +1,17 @@ + +extern "C" { + fn answer() -> u32; +} + +// For the purpose of this wasm example, we don't worry about multi-threading, +// and will be using the PLACE in unsafe manner below. +static mut PLACE: u32 = 23; + +#[no_mangle] +pub extern fn bar() -> *const u32 { + unsafe { + PLACE = answer(); + // Return a pointer to the exported memory. + (&PLACE) as *const u32 + } +} diff --git a/misc/wasmtime-py/examples/two_modules/run.py b/misc/wasmtime-py/examples/two_modules/run.py new file mode 100644 index 0000000000..f5b0aa6c25 --- /dev/null +++ b/misc/wasmtime-py/examples/two_modules/run.py @@ -0,0 +1,4 @@ +import wasmtime +import two + +print("answer() returned", two.ask()) diff --git a/misc/wasmtime-py/examples/two_modules/two.wat b/misc/wasmtime-py/examples/two_modules/two.wat new file mode 100644 index 0000000000..265d633284 --- /dev/null +++ b/misc/wasmtime-py/examples/two_modules/two.wat @@ -0,0 +1,11 @@ +(module + (import "one" "memory" (memory $memory 0)) + (import "one" "bar" (func $bar (result i32))) + (export "ask" (func $foo)) + + (func $foo (result i32) + call $bar + ;; Deference returned pointer to the value from imported memory + i32.load + ) +) diff --git a/misc/wasmtime-py/python/wasmtime/__init__.py b/misc/wasmtime-py/python/wasmtime/__init__.py new file mode 100644 index 0000000000..8c611175fd --- /dev/null +++ b/misc/wasmtime-py/python/wasmtime/__init__.py @@ -0,0 +1,49 @@ +from .lib_wasmtime import imported_modules, instantiate +import sys +import os.path + +from importlib import import_module +from importlib.abc import Loader, MetaPathFinder +from importlib.util import spec_from_file_location + +# Mostly copied from +# https://stackoverflow.com/questions/43571737/how-to-implement-an-import-hook-that-can-modify-the-source-code-on-the-fly-using +class MyMetaFinder(MetaPathFinder): + def find_spec(self, fullname, path, target=None): + if path is None or path == "": + path = [os.getcwd()] # top level import -- + path.extend(sys.path) + if "." in fullname: + *parents, name = fullname.split(".") + else: + name = fullname + for entry in path: + filename = os.path.join(entry, name + ".wasm") + if not os.path.exists(filename): + continue + + return spec_from_file_location(fullname, filename, loader=MyLoader(filename)) + return None + +class MyLoader(Loader): + def __init__(self, filename): + self.filename = filename + + def create_module(self, spec): + return None # use default module creation semantics + + def exec_module(self, module): + with open(self.filename, "rb") as f: + data = f.read() + + imports = {} + for module_name, fields in imported_modules(data).items(): + imports[module_name] = {} + imported_module = import_module(module_name) + for field_name in fields: + imports[module_name][field_name] = imported_module.__dict__[field_name] + + res = instantiate(data, imports) + module.__dict__.update(res.instance.exports) + +sys.meta_path.insert(0, MyMetaFinder()) diff --git a/misc/wasmtime-py/setup.py b/misc/wasmtime-py/setup.py new file mode 100644 index 0000000000..3737e89d11 --- /dev/null +++ b/misc/wasmtime-py/setup.py @@ -0,0 +1,18 @@ +from setuptools import setup +from setuptools_rust import Binding, RustExtension + +setup(name='wasmtime', + version="0.0.1", + classifiers=[ + "Development Status :: 1 - Planning", + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Rust", + "Operating System :: POSIX", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", + ], + packages=['wasmtime'], + package_dir={'wasmtime': 'python/wasmtime'}, + rust_extensions=[RustExtension('wasmtime.lib_wasmtime', 'Cargo.toml', binding=Binding.PyO3)], + zip_safe=False) diff --git a/misc/wasmtime-py/src/code_memory.rs b/misc/wasmtime-py/src/code_memory.rs new file mode 100644 index 0000000000..b117f52289 --- /dev/null +++ b/misc/wasmtime-py/src/code_memory.rs @@ -0,0 +1,83 @@ +//! Memory management for executable code. +// Copy of wasmtime's wasmtime-jit/src/code_memory.rs + +use core::{cmp, mem}; +use region; +use std::string::String; +use std::vec::Vec; +use wasmtime_runtime::{Mmap, VMFunctionBody}; + +/// Memory manager for executable code. +pub(crate) struct CodeMemory { + current: Mmap, + mmaps: Vec, + position: usize, + published: usize, +} + +impl CodeMemory { + /// Create a new `CodeMemory` instance. + pub fn new() -> Self { + Self { + current: Mmap::new(), + mmaps: Vec::new(), + position: 0, + published: 0, + } + } + + /// 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], String> { + if self.current.len() - self.position < size { + self.mmaps.push(mem::replace( + &mut self.current, + Mmap::with_at_least(cmp::max(0x10000, size))?, + )); + self.position = 0; + } + let old_position = self.position; + self.position += size; + Ok(&mut self.current.as_mut_slice()[old_position..self.position]) + } + + /// 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 } + } + + /// Allocate enough memory to hold a copy of `slice` and copy the data into it. + /// 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_copy_of_byte_slice( + &mut self, + slice: &[u8], + ) -> Result<&mut [VMFunctionBody], String> { + let new = self.allocate(slice.len())?; + new.copy_from_slice(slice); + Ok(Self::view_as_mut_vmfunc_slice(new)) + } + + /// Make all allocated memory executable. + pub fn publish(&mut self) { + self.mmaps + .push(mem::replace(&mut self.current, Mmap::new())); + self.position = 0; + + for m 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"); + } + } + self.published = self.mmaps.len(); + } +} diff --git a/misc/wasmtime-py/src/function.rs b/misc/wasmtime-py/src/function.rs new file mode 100644 index 0000000000..887bcbc896 --- /dev/null +++ b/misc/wasmtime-py/src/function.rs @@ -0,0 +1,66 @@ +//! Support for a calling of a bounds (exported) function. + +use pyo3::prelude::*; +use pyo3::types::PyTuple; + +use crate::value::{pyobj_to_value, value_to_pyobj}; +use std::cell::RefCell; +use std::rc::Rc; + +use cranelift_codegen::ir; +use wasmtime_interface_types::ModuleData; +use wasmtime_jit::{Context, InstanceHandle}; +use wasmtime_runtime::Export; + +// TODO support non-export functions +#[pyclass] +pub struct Function { + pub context: Rc>, + pub instance: InstanceHandle, + pub export_name: String, + pub args_types: Vec, + pub data: Rc, +} + +impl Function { + pub fn get_signature(&self) -> ir::Signature { + let mut instance = self.instance.clone(); + if let Some(Export::Function { signature, .. }) = instance.lookup(&self.export_name) { + signature + } else { + panic!() + } + } +} + +#[pymethods] +impl Function { + #[__call__] + #[args(args = "*")] + fn call(&self, py: Python, args: &PyTuple) -> PyResult { + let mut runtime_args = Vec::new(); + for item in args.iter() { + runtime_args.push(pyobj_to_value(py, item)?); + } + let mut instance = self.instance.clone(); + let mut cx = self.context.borrow_mut(); + let results = self + .data + .invoke( + &mut cx, + &mut instance, + self.export_name.as_str(), + &runtime_args, + ) + .map_err(crate::err2py)?; + let mut py_results = Vec::new(); + for result in results { + py_results.push(value_to_pyobj(py, result)?); + } + if py_results.len() == 1 { + Ok(py_results[0].clone_ref(py)) + } else { + Ok(PyTuple::new(py, py_results).to_object(py)) + } + } +} diff --git a/misc/wasmtime-py/src/import.rs b/misc/wasmtime-py/src/import.rs new file mode 100644 index 0000000000..f5f66fd42e --- /dev/null +++ b/misc/wasmtime-py/src/import.rs @@ -0,0 +1,357 @@ +//! Support for a calling of an imported function. + +use pyo3::prelude::*; +use pyo3::types::{PyAny, PyDict, PyTuple}; + +use crate::code_memory::CodeMemory; +use crate::function::Function; +use crate::memory::Memory; +use crate::value::{read_value_from, write_value_to}; +use cranelift_codegen::ir::types; +use cranelift_codegen::ir::{InstBuilder, StackSlotData, StackSlotKind}; +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 target_lexicon::HOST; +use wasmtime_environ::{Export, Module}; +use wasmtime_runtime::{Imports, InstanceHandle, VMContext, VMFunctionBody}; + +use core::cmp; +use std::cell::RefCell; +use std::collections::{HashMap, HashSet}; +use std::rc::Rc; + +struct BoundPyFunction { + name: String, + obj: PyObject, +} + +struct ImportObjState { + calls: Vec, + #[allow(dead_code)] + code_memory: CodeMemory, +} + +unsafe extern "C" fn stub_fn(vmctx: *mut VMContext, call_id: u32, values_vec: *mut i64) { + let gil = Python::acquire_gil(); + let py = gil.python(); + + let mut instance = InstanceHandle::from_vmctx(vmctx); + let (_name, obj) = { + let state = instance + .host_state() + .downcast_mut::() + .expect("state"); + let name = state.calls[call_id as usize].name.to_owned(); + let obj = state.calls[call_id as usize].obj.clone_ref(py); + (name, obj) + }; + 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(read_value_from( + py, + values_vec.offset(i as isize - 1), + signature.params[i].value_type, + )) + } + let result = obj.call(py, PyTuple::new(py, args), None).expect("result"); + for i in 0..signature.returns.len() { + let val = if result.is_none() { + 0.into_object(py) // FIXME default ??? + } else { + if i > 0 { + panic!("multiple returns unsupported"); + } + result.clone_ref(py) + }; + write_value_to( + py, + values_vec.offset(i as isize), + signature.returns[i].value_type, + val, + ); + } +} + +/// Create a trampoline for invoking a python function. +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)); + + 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); + builder + .ins() + .call_indirect(new_sig, callee_value, &callee_args); + + 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 = Vec::new(); + let mut reloc_sink = RelocSink {}; + let mut trap_sink = binemit::NullTrapSink {}; + context + .compile_and_emit(isa, &mut code_buf, &mut reloc_sink, &mut trap_sink) + .expect("compile_and_emit"); + + code_memory + .allocate_copy_of_byte_slice(&code_buf) + .expect("allocate_copy_of_byte_slice") + .as_ptr() +} + +fn parse_annotation_type(s: &str) -> ir::Type { + match s { + "I32" | "i32" => types::I32, + "I64" | "i64" => types::I64, + "F32" | "f32" => types::F32, + "F64" | "f64" => types::F64, + _ => panic!("unknown type in annotations"), + } +} + +fn get_signature_from_py_annotation( + annot: &PyDict, + pointer_type: ir::Type, + call_conv: isa::CallConv, +) -> PyResult { + let mut params = Vec::new(); + params.push(ir::AbiParam::special( + pointer_type, + ir::ArgumentPurpose::VMContext, + )); + let mut returns = None; + for (name, value) in annot.iter() { + let ty = parse_annotation_type(&value.to_string()); + match name.to_string().as_str() { + "return" => returns = Some(ty), + _ => params.push(ir::AbiParam::new(ty)), + } + } + Ok(ir::Signature { + params, + returns: match returns { + Some(r) => vec![ir::AbiParam::new(r)], + None => vec![], + }, + call_conv, + }) +} + +pub fn into_instance_from_obj( + py: Python, + global_exports: Rc>>>, + obj: &PyAny, +) -> PyResult { + 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 = + 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 obj = obj.cast_as::()?; + let mut bound_functions = Vec::new(); + let mut dependencies = HashSet::new(); + let mut memories = PrimaryMap::new(); + for (name, item) in obj.iter() { + if item.is_callable() { + let sig = if item.get_type().is_subclass::()? { + // TODO faster calls? + let wasm_fn = item.cast_as::()?; + dependencies.insert(wasm_fn.instance.clone()); + wasm_fn.get_signature() + } else if item.hasattr("__annotations__")? { + let annot = item.getattr("__annotations__")?.cast_as::()?; + get_signature_from_py_annotation(&annot, pointer_type, call_conv)? + } else { + // TODO support calls without annotations? + continue; + }; + + let sig_id = module.signatures.push(sig.clone()); + let func_id = module.functions.push(sig_id); + module + .exports + .insert(name.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, + ); + finished_functions.push(trampoline); + + bound_functions.push(BoundPyFunction { + name: name.to_string(), + obj: item.into_object(py), + }); + } else if item.get_type().is_subclass::()? { + let wasm_mem = item.cast_as::()?; + dependencies.insert(wasm_mem.instance.clone()); + let plan = wasm_mem.get_plan(); + let mem_id = module.memory_plans.push(plan); + let _mem_id_2 = memories.push(wasm_mem.into_import()); + assert_eq!(mem_id, _mem_id_2); + let _mem_id_3 = module + .imported_memories + .push((String::from(""), String::from(""))); + assert_eq!(mem_id, _mem_id_3); + module + .exports + .insert(name.to_string(), Export::Memory(mem_id)); + } + } + + let imports = Imports::new( + dependencies, + PrimaryMap::new(), + PrimaryMap::new(), + memories, + PrimaryMap::new(), + ); + let data_initializers = Vec::new(); + let signatures = PrimaryMap::new(); + + code_memory.publish(); + + let import_obj_state = ImportObjState { + calls: bound_functions, + code_memory, + }; + + Ok(InstanceHandle::new( + Rc::new(module), + global_exports, + finished_functions.into_boxed_slice(), + imports, + &data_initializers, + signatures.into_boxed_slice(), + None, + Box::new(import_obj_state), + ) + .expect("instance")) +} + +/// 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_jt( + &mut self, + _offset: binemit::CodeOffset, + _reloc: binemit::Reloc, + _jt: ir::JumpTable, + ) { + panic!("trampoline compilation should not produce jump table relocs"); + } +} diff --git a/misc/wasmtime-py/src/instance.rs b/misc/wasmtime-py/src/instance.rs new file mode 100644 index 0000000000..98307f7465 --- /dev/null +++ b/misc/wasmtime-py/src/instance.rs @@ -0,0 +1,106 @@ +//! WebAssembly Instance API object. + +use pyo3::prelude::*; +use pyo3::types::PyDict; + +use crate::function::Function; +use crate::memory::Memory; +use std::cell::RefCell; +use std::rc::Rc; + +use cranelift_codegen::ir; +use cranelift_codegen::ir::types; +use wasmtime_environ::Export; +use wasmtime_interface_types::ModuleData; +use wasmtime_jit::{Context, InstanceHandle}; +use wasmtime_runtime::Export as RuntimeExport; + +#[pyclass] +pub struct Instance { + pub context: Rc>, + pub instance: InstanceHandle, + pub data: Rc, +} + +fn get_type_annot(ty: ir::Type) -> &'static str { + match ty { + types::I32 => "i32", + types::I64 => "i64", + types::F32 => "f32", + types::F64 => "f64", + _ => panic!("unknown type"), + } +} + +#[pymethods] +impl Instance { + #[getter(exports)] + fn get_exports(&mut self) -> PyResult { + let gil = Python::acquire_gil(); + let py = gil.python(); + let exports = PyDict::new(py); + let mut function_exports = Vec::new(); + let mut memory_exports = Vec::new(); + for (name, export) in self.instance.exports() { + match export { + Export::Function(_) => function_exports.push(name.to_string()), + Export::Memory(_) => memory_exports.push(name.to_string()), + _ => { + // Skip unknown export type. + continue; + } + } + } + for name in memory_exports { + if let Some(RuntimeExport::Memory { .. }) = self.instance.lookup(&name) { + let f = Py::new( + py, + Memory { + context: self.context.clone(), + instance: self.instance.clone(), + export_name: name.clone(), + }, + )?; + exports.set_item(name, f)?; + } else { + panic!("memory"); + } + } + for name in function_exports { + if let Some(RuntimeExport::Function { signature, .. }) = self.instance.lookup(&name) { + let annot = PyDict::new(py); + let mut args_types = Vec::new(); + for index in 1..signature.params.len() { + let ty = signature.params[index].value_type; + args_types.push(ty); + annot.set_item(format!("param{}", index - 1), get_type_annot(ty))?; + } + match signature.returns.len() { + 0 => (), + 1 => { + annot + .set_item("return", get_type_annot(signature.returns[0].value_type))?; + } + _ => panic!("multi-return"), + } + let f = Py::new( + py, + Function { + context: self.context.clone(), + instance: self.instance.clone(), + data: self.data.clone(), + export_name: name.clone(), + args_types, + }, + )?; + // FIXME set the f object the `__annotations__` attribute somehow? + let _ = annot; + exports.set_item(name, f)?; + } else { + panic!("function"); + } + } + + Ok(exports.to_object(py)) + } +} diff --git a/misc/wasmtime-py/src/lib.rs b/misc/wasmtime-py/src/lib.rs new file mode 100644 index 0000000000..f35fa7b1bb --- /dev/null +++ b/misc/wasmtime-py/src/lib.rs @@ -0,0 +1,145 @@ +use pyo3::exceptions::Exception; +use pyo3::prelude::*; +use pyo3::types::{PyBytes, PyDict, PySet}; +use pyo3::wrap_pyfunction; + +use crate::import::into_instance_from_obj; +use crate::instance::Instance; +use crate::memory::Memory; +use crate::module::Module; +use std::cell::RefCell; +use std::rc::Rc; +use wasmtime_interface_types::ModuleData; + +mod code_memory; +mod function; +mod import; +mod instance; +mod memory; +mod module; +mod value; + +fn err2py(err: failure::Error) -> PyErr { + let mut desc = err.to_string(); + for cause in err.iter_causes() { + desc.push_str("\n"); + desc.push_str(" caused by: "); + desc.push_str(&cause.to_string()); + } + PyErr::new::(desc) +} + +#[pyclass] +pub struct InstantiateResultObject { + instance: Py, + module: Py, +} + +#[pymethods] +impl InstantiateResultObject { + #[getter(instance)] + fn get_instance(&self) -> PyResult> { + let gil = Python::acquire_gil(); + let py = gil.python(); + Ok(self.instance.clone_ref(py)) + } + + #[getter(module)] + fn get_module(&self) -> PyResult> { + let gil = Python::acquire_gil(); + let py = gil.python(); + Ok(self.module.clone_ref(py)) + } +} + +/// WebAssembly instantiate API method. +#[pyfunction] +pub fn instantiate( + py: Python, + buffer_source: &PyBytes, + import_obj: &PyDict, +) -> PyResult> { + let wasm_data = buffer_source.as_bytes(); + + let generate_debug_info = false; + + let isa = { + let isa_builder = cranelift_native::builder().map_err(|s| PyErr::new::(s))?; + let flag_builder = cranelift_codegen::settings::builder(); + isa_builder.finish(cranelift_codegen::settings::Flags::new(flag_builder)) + }; + + let mut context = wasmtime_jit::Context::with_isa(isa); + context.set_debug_info(generate_debug_info); + let global_exports = context.get_global_exports(); + + for (name, obj) in import_obj.iter() { + context.name_instance( + name.to_string(), + into_instance_from_obj(py, global_exports.clone(), obj)?, + ) + } + + let data = Rc::new(ModuleData::new(wasm_data).map_err(err2py)?); + let instance = context + .instantiate_module(None, wasm_data) + .map_err(|e| err2py(e.into()))?; + + let module = Py::new( + py, + Module { + module: instance.module(), + }, + )?; + + let instance = Py::new( + py, + Instance { + context: Rc::new(RefCell::new(context)), + instance, + data, + }, + )?; + + Py::new(py, InstantiateResultObject { instance, module }) +} + +#[pyfunction] +pub fn imported_modules<'p>(py: Python<'p>, buffer_source: &PyBytes) -> PyResult<&'p PyDict> { + let wasm_data = buffer_source.as_bytes(); + let dict = PyDict::new(py); + // TODO: error handling + let mut parser = wasmparser::ModuleReader::new(wasm_data).unwrap(); + while !parser.eof() { + let section = parser.read().unwrap(); + match section.code { + wasmparser::SectionCode::Import => {} + _ => continue, + }; + let reader = section.get_import_section_reader().unwrap(); + for import in reader { + let import = import.unwrap(); + let set = match dict.get_item(import.module) { + Some(set) => set.downcast_ref::().unwrap(), + None => { + let set = PySet::new::(py, &[])?; + dict.set_item(import.module, set)?; + set + } + }; + set.add(import.field)?; + } + } + Ok(dict) +} + +#[pymodule] +fn lib_wasmtime(_: Python, m: &PyModule) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_wrapped(wrap_pyfunction!(instantiate))?; + m.add_wrapped(wrap_pyfunction!(imported_modules))?; + Ok(()) +} diff --git a/misc/wasmtime-py/src/memory.rs b/misc/wasmtime-py/src/memory.rs new file mode 100644 index 0000000000..072e45cb8f --- /dev/null +++ b/misc/wasmtime-py/src/memory.rs @@ -0,0 +1,131 @@ +//! WebAssembly Memory API object. + +use pyo3::class::PyBufferProtocol; +use pyo3::exceptions::BufferError; +use pyo3::ffi; +use pyo3::prelude::*; + +use std::cell::RefCell; +use std::ffi::CStr; +use std::os::raw::{c_int, c_void}; +use std::ptr; +use std::rc::Rc; + +use wasmtime_environ::MemoryPlan; +use wasmtime_jit::{Context, InstanceHandle}; +use wasmtime_runtime::{Export, VMMemoryDefinition, VMMemoryImport}; + +#[pyclass] +pub struct Memory { + pub context: Rc>, + pub instance: InstanceHandle, + pub export_name: String, +} + +impl Memory { + fn descriptor(&self) -> *mut VMMemoryDefinition { + let mut instance = self.instance.clone(); + if let Some(Export::Memory { definition, .. }) = instance.lookup(&self.export_name) { + definition + } else { + panic!("memory is expected"); + } + } +} + +impl Memory { + pub fn get_plan(&self) -> MemoryPlan { + let mut instance = self.instance.clone(); + if let Some(Export::Memory { memory, .. }) = instance.lookup(&self.export_name) { + memory + } else { + panic!() + } + } + + pub fn into_import(&self) -> VMMemoryImport { + let mut instance = self.instance.clone(); + if let Some(Export::Memory { + definition, vmctx, .. + }) = instance.lookup(&self.export_name) + { + VMMemoryImport { + from: definition, + vmctx, + } + } else { + panic!() + } + } +} + +#[pymethods] +impl Memory { + #[getter(current)] + pub fn current(&self) -> u32 { + let current_length = unsafe { (*self.descriptor()).current_length }; + (current_length >> 16) as u32 + } + + pub fn grow(&self, _number: u32) -> u32 { + (-1i32) as u32 + } +} + +#[pyproto] +impl PyBufferProtocol for Memory { + fn bf_getbuffer(&self, view: *mut ffi::Py_buffer, flags: c_int) -> PyResult<()> { + if view.is_null() { + return Err(BufferError::py_err("View is null")); + } + + unsafe { + /* + As a special case, for temporary buffers that are wrapped by + PyMemoryView_FromBuffer() or PyBuffer_FillInfo() this field is NULL. + In general, exporting objects MUST NOT use this scheme. + */ + (*view).obj = ptr::null_mut(); + } + + let readonly = if (flags & ffi::PyBUF_WRITABLE) == ffi::PyBUF_WRITABLE { + 0 + } else { + 1 + }; + + let VMMemoryDefinition { + base, + current_length, + } = unsafe { *self.descriptor() }; + + unsafe { + (*view).buf = base as *mut c_void; + (*view).len = current_length as isize; + (*view).readonly = readonly; + (*view).itemsize = 1; + + (*view).format = ptr::null_mut(); + if (flags & ffi::PyBUF_FORMAT) == ffi::PyBUF_FORMAT { + let msg = CStr::from_bytes_with_nul(b"B\0").unwrap(); + (*view).format = msg.as_ptr() as *mut _; + } + + (*view).ndim = 1; + (*view).shape = ptr::null_mut(); + if (flags & ffi::PyBUF_ND) == ffi::PyBUF_ND { + (*view).shape = (&((*view).len)) as *const _ as *mut _; + } + + (*view).strides = ptr::null_mut(); + if (flags & ffi::PyBUF_STRIDES) == ffi::PyBUF_STRIDES { + (*view).strides = &((*view).itemsize) as *const _ as *mut _; + } + + (*view).suboffsets = ptr::null_mut(); + (*view).internal = ptr::null_mut(); + } + + Ok(()) + } +} diff --git a/misc/wasmtime-py/src/module.rs b/misc/wasmtime-py/src/module.rs new file mode 100644 index 0000000000..9aecdee883 --- /dev/null +++ b/misc/wasmtime-py/src/module.rs @@ -0,0 +1,10 @@ +//! WebAssembly Module API object. + +use pyo3::prelude::*; + +use std::rc::Rc; + +#[pyclass] +pub struct Module { + pub module: Rc, +} diff --git a/misc/wasmtime-py/src/value.rs b/misc/wasmtime-py/src/value.rs new file mode 100644 index 0000000000..21512189b0 --- /dev/null +++ b/misc/wasmtime-py/src/value.rs @@ -0,0 +1,61 @@ +//! Utility functions to handle values conversion between abstractions/targets. + +use pyo3::exceptions::Exception; +use pyo3::prelude::*; +use pyo3::types::PyAny; + +use cranelift_codegen::ir; +use std::ptr; +use wasmtime_interface_types::Value; + +pub fn pyobj_to_value(_: Python, p: &PyAny) -> PyResult { + if let Ok(n) = p.extract() { + Ok(Value::I32(n)) + } else if let Ok(n) = p.extract() { + Ok(Value::U32(n)) + } else if let Ok(n) = p.extract() { + Ok(Value::I64(n)) + } else if let Ok(n) = p.extract() { + Ok(Value::U64(n)) + } else if let Ok(n) = p.extract() { + Ok(Value::F64(n)) + } else if let Ok(n) = p.extract() { + Ok(Value::F32(n)) + } else if let Ok(s) = p.extract() { + Ok(Value::String(s)) + } else { + Err(PyErr::new::("unsupported value type")) + } +} + +pub fn value_to_pyobj(py: Python, value: Value) -> PyResult { + Ok(match value { + Value::I32(i) => i.into_object(py), + Value::U32(i) => i.into_object(py), + Value::I64(i) => i.into_object(py), + Value::U64(i) => i.into_object(py), + Value::F32(i) => i.into_object(py), + Value::F64(i) => i.into_object(py), + Value::String(i) => i.into_object(py), + }) +} + +pub unsafe fn read_value_from(py: Python, ptr: *mut i64, ty: ir::Type) -> PyObject { + match ty { + ir::types::I32 => ptr::read(ptr as *const i32).into_object(py), + ir::types::I64 => ptr::read(ptr as *const i64).into_object(py), + ir::types::F32 => ptr::read(ptr as *const f32).into_object(py), + ir::types::F64 => ptr::read(ptr as *const f64).into_object(py), + _ => panic!("TODO add PyResult to read_value_from"), + } +} + +pub unsafe fn write_value_to(py: Python, ptr: *mut i64, ty: ir::Type, val: PyObject) { + match ty { + ir::types::I32 => ptr::write(ptr as *mut i32, val.extract::(py).expect("i32")), + ir::types::I64 => ptr::write(ptr as *mut i64, val.extract::(py).expect("i64")), + ir::types::F32 => ptr::write(ptr as *mut f32, val.extract::(py).expect("f32")), + ir::types::F64 => ptr::write(ptr as *mut f64, val.extract::(py).expect("f64")), + _ => panic!("TODO add PyResult to write_value_to"), + } +} diff --git a/src/bin/wasmtime.rs b/src/bin/wasmtime.rs index 7ff144c04d..c616fb23b7 100644 --- a/src/bin/wasmtime.rs +++ b/src/bin/wasmtime.rs @@ -34,20 +34,19 @@ use cranelift_codegen::settings; use cranelift_codegen::settings::Configurable; use cranelift_native; use docopt::Docopt; +use failure::{bail, format_err, Error, ResultExt}; use pretty_env_logger; use serde::Deserialize; -use std::error::Error; use std::ffi::OsStr; use std::fs::File; -use std::io; -use std::io::prelude::*; use std::path::Component; use std::path::{Path, PathBuf}; use std::process::exit; use wabt; use wasi_common::preopen_dir; use wasmtime_environ::cache_conf; -use wasmtime_jit::{ActionOutcome, Context, Features}; +use wasmtime_interface_types::ModuleData; +use wasmtime_jit::{Context, Features, InstanceHandle}; use wasmtime_wasi::instantiate_wasi; use wasmtime_wast::instantiate_spectest; @@ -100,22 +99,16 @@ struct Args { flag_wasi_c: bool, } -fn read_to_end(path: PathBuf) -> Result, io::Error> { - let mut buf: Vec = Vec::new(); - let mut file = File::open(path)?; - file.read_to_end(&mut buf)?; - Ok(buf) -} - -fn read_wasm(path: PathBuf) -> Result, String> { - let data = read_to_end(path).map_err(|err| err.to_string())?; +fn read_wasm(path: PathBuf) -> Result, Error> { + let data = std::fs::read(&path) + .with_context(|_| format!("failed to read file: {}", path.display()))?; // If data is a wasm binary, use that. If it's using wat format, convert it // to a wasm binary with wat2wasm. Ok(if data.starts_with(&[b'\0', b'a', b's', b'm']) { data } else { - wabt::wat2wasm(data).map_err(|err| String::from(err.description()))? + wabt::wat2wasm(data)? }) } @@ -191,6 +184,18 @@ fn compute_environ(flag_env: &[String]) -> Vec<(String, String)> { } fn main() { + let err = match rmain() { + Ok(()) => return, + Err(e) => e, + }; + eprintln!("error: {}", err); + for cause in err.iter_causes() { + eprintln!(" caused by: {}", cause); + } + std::process::exit(1); +} + +fn rmain() -> Result<(), Error> { let version = env!("CARGO_PKG_VERSION"); let args: Args = Docopt::new(USAGE) .and_then(|d| { @@ -208,36 +213,32 @@ fn main() { cache_conf::init(args.flag_cache); - let isa_builder = cranelift_native::builder().unwrap_or_else(|_| { - panic!("host machine is not a supported target"); - }); + let isa_builder = cranelift_native::builder() + .map_err(|s| format_err!("host machine is not a supported target: {}", s))?; let mut flag_builder = settings::builder(); let mut features: Features = Default::default(); // Enable verifier passes in debug mode. if cfg!(debug_assertions) { - flag_builder.enable("enable_verifier").unwrap(); + flag_builder.enable("enable_verifier")?; } // Enable SIMD if requested if args.flag_enable_simd { - flag_builder.enable("enable_simd").unwrap(); + flag_builder.enable("enable_simd")?; features.simd = true; } // Enable optimization if requested. if args.flag_optimize { - flag_builder.set("opt_level", "best").unwrap(); + flag_builder.set("opt_level", "best")?; } let isa = isa_builder.finish(settings::Flags::new(flag_builder)); let mut context = Context::with_isa(isa).with_features(features); // Make spectest available by default. - context.name_instance( - "spectest".to_owned(), - instantiate_spectest().expect("instantiating spectest"), - ); + context.name_instance("spectest".to_owned(), instantiate_spectest()?); // Make wasi available by default. let global_exports = context.get_global_exports(); @@ -248,16 +249,15 @@ fn main() { let wasi = if args.flag_wasi_c { #[cfg(feature = "wasi-c")] { - instantiate_wasi_c("", global_exports, &preopen_dirs, &argv, &environ) + instantiate_wasi_c("", global_exports, &preopen_dirs, &argv, &environ)? } #[cfg(not(feature = "wasi-c"))] { - panic!("wasi-c feature not enabled at build time") + bail!("wasi-c feature not enabled at build time") } } else { - instantiate_wasi("", global_exports, &preopen_dirs, &argv, &environ) - } - .expect("instantiating wasi"); + instantiate_wasi("", global_exports, &preopen_dirs, &argv, &environ)? + }; context.name_instance("wasi_unstable".to_owned(), wasi); @@ -267,48 +267,102 @@ fn main() { // Load the preload wasm modules. for filename in &args.flag_preload { let path = Path::new(&filename); - match handle_module(&mut context, &args, path) { - Ok(()) => {} - Err(message) => { - let name = path.as_os_str().to_string_lossy(); - println!("error while processing preload {}: {}", name, message); - exit(1); - } - } + instantiate_module(&mut context, path) + .with_context(|_| format!("failed to process preload at `{}`", path.display()))?; } // Load the main wasm module. let path = Path::new(&args.arg_file); - match handle_module(&mut context, &args, path) { - Ok(()) => {} - Err(message) => { - let name = path.as_os_str().to_string_lossy(); - println!("error while processing main module {}: {}", name, message); - exit(1); - } - } + handle_module(&mut context, &args, path) + .with_context(|_| format!("failed to process main module `{}`", path.display()))?; + Ok(()) } -fn handle_module(context: &mut Context, args: &Args, path: &Path) -> Result<(), String> { +fn instantiate_module( + context: &mut Context, + path: &Path, +) -> Result<(InstanceHandle, Vec), Error> { // Read the wasm module binary. let data = read_wasm(path.to_path_buf())?; // Compile and instantiating a wasm module. - let mut instance = context - .instantiate_module(None, &data) - .map_err(|e| e.to_string())?; + let handle = context.instantiate_module(None, &data)?; + Ok((handle, data)) +} + +fn handle_module(context: &mut Context, args: &Args, path: &Path) -> Result<(), Error> { + let (mut instance, data) = instantiate_module(context, path)?; // If a function to invoke was given, invoke it. - if let Some(ref f) = args.flag_invoke { - match context - .invoke(&mut instance, f, &[]) - .map_err(|e| e.to_string())? - { - ActionOutcome::Returned { .. } => {} - ActionOutcome::Trapped { message } => { - return Err(format!("Trap from within function {}: {}", f, message)); - } - } + if let Some(f) = &args.flag_invoke { + let data = ModuleData::new(&data)?; + invoke_export(context, &mut instance, &data, f, args)?; + } + + Ok(()) +} + +fn invoke_export( + context: &mut Context, + instance: &mut InstanceHandle, + data: &ModuleData, + name: &str, + args: &Args, +) -> Result<(), Error> { + use wasm_webidl_bindings::ast; + use wasmtime_interface_types::Value; + + // Use the binding information in `ModuleData` to figure out what arguments + // need to be passed to the function that we're invoking. Currently we take + // the CLI parameters and attempt to parse them into function arguments for + // the function we'll invoke. + let binding = data.binding_for_export(instance, name)?; + if binding.param_types()?.len() > 0 { + eprintln!( + "warning: using `--render` with a function that takes arguments \ + is experimental and may break in the future" + ); + } + let mut values = Vec::new(); + let mut args = args.arg_arg.iter(); + for ty in binding.param_types()? { + let val = match args.next() { + Some(s) => s, + None => bail!("not enough arguments for `{}`", name), + }; + values.push(match ty { + // TODO: integer parsing here should handle hexadecimal notation + // like `0x0...`, but the Rust standard library currently only + // parses base-10 representations. + ast::WebidlScalarType::Long => Value::I32(val.parse()?), + ast::WebidlScalarType::LongLong => Value::I64(val.parse()?), + ast::WebidlScalarType::UnsignedLong => Value::U32(val.parse()?), + ast::WebidlScalarType::UnsignedLongLong => Value::U64(val.parse()?), + + ast::WebidlScalarType::Float | ast::WebidlScalarType::UnrestrictedFloat => { + Value::F32(val.parse()?) + } + ast::WebidlScalarType::Double | ast::WebidlScalarType::UnrestrictedDouble => { + Value::F64(val.parse()?) + } + ast::WebidlScalarType::DomString => Value::String(val.to_string()), + t => bail!("unsupported argument type {:?}", t), + }); + } + + // Invoke the function and then afterwards print all the results that came + // out, if there are any. + let results = data + .invoke(context, instance, name, &values) + .with_context(|_| format!("failed to invoke `{}`", name))?; + if results.len() > 0 { + eprintln!( + "warning: using `--render` with a function that returns values \ + is experimental and may break in the future" + ); + } + for result in results { + println!("{}", result); } Ok(()) diff --git a/wasmtime-interface-types/Cargo.toml b/wasmtime-interface-types/Cargo.toml new file mode 100644 index 0000000000..e1aa1f826e --- /dev/null +++ b/wasmtime-interface-types/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "wasmtime-interface-types" +version = "0.1.0" +authors = ["The Wasmtime Project Developers"] +publish = false +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] +cranelift-codegen = "0.38.0" +failure = "0.1" +walrus = "0.11.0" +wasmparser = "0.35" +wasm-webidl-bindings = "0.4.0" +wasmtime-jit = { path = '../wasmtime-jit' } +wasmtime-runtime = { path = '../wasmtime-runtime' } diff --git a/wasmtime-interface-types/src/lib.rs b/wasmtime-interface-types/src/lib.rs new file mode 100644 index 0000000000..51e9f16c78 --- /dev/null +++ b/wasmtime-interface-types/src/lib.rs @@ -0,0 +1,499 @@ +//! 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)] + +use cranelift_codegen::ir; +use failure::{bail, format_err, Error}; +use std::convert::TryFrom; +use std::slice; +use std::str; +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, +} + +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 { + // 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 }), + }) + } + + /// 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, Error> { + let binding = self.binding_for_export(handle, export)?; + let incoming = binding.param_bindings()?; + let outgoing = binding.result_bindings()?; + + // We have a magical dummy binding which indicates that this wasm + // function is using a return pointer. This is a total hack around + // multi-value, and we really should just implement multi-value in + // wasm-bindgen. In the meantime though this synthesizes a return + // pointer going as the first argument and translating outgoing + // arguments reads from the return pointer. + let (base, incoming, outgoing) = if uses_retptr(&outgoing) { + (Some(8), &incoming[1..], &outgoing[1..]) + } else { + (None, incoming.as_slice(), outgoing.as_slice()) + }; + let mut wasm_args = translate_incoming(cx, handle, &incoming, base.is_some() as u32, args)?; + if let Some(n) = base { + wasm_args.insert(0, RuntimeValue::I32(n as i32)); + } + 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, base, &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, Error> { + 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> { + let inner = self.inner.as_ref()?; + let bindings = inner.module.customs.get_typed::()?; + 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, Error> { + 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, Error> { + 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::(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"), + }; + let skip = if uses_retptr(&binding.result.bindings) { + 1 + } else { + 0 + }; + func.params + .iter() + .skip(skip) + .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, Error> { + 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 { + 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 { + let ty = abi2ast(param)?; + Ok(ast::OutgoingBindingExpressionAs { + ty: ty.into(), + idx: idx as u32, + } + .into()) +} + +fn abi2ast(param: &ir::AbiParam) -> Result { + 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], + offset: u32, + args: &[Value], +) -> Result, Error> { + let get = |expr: &ast::IncomingBindingExpression| match expr { + ast::IncomingBindingExpression::Get(g) => args + .get((g.idx - offset) 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], + retptr: Option, + args: &[RuntimeValue], +) -> Result, Error> { + 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, + )) + }; + + if retptr.is_some() { + assert!(args.is_empty()); + } + + let get = |idx: u32| match retptr { + Some(i) => { + let bytes = raw_memory()?; + let base = &bytes[(i + idx * 4) as usize..][..4]; + Ok(RuntimeValue::I32( + ((base[0] as i32) << 0) + | ((base[1] as i32) << 8) + | ((base[2] as i32) << 16) + | ((base[3] as i32) << 24), + )) + } + None => 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) +} + +fn uses_retptr(outgoing: &[ast::OutgoingBindingExpression]) -> bool { + match outgoing.get(0) { + Some(ast::OutgoingBindingExpression::As(e)) => e.idx == u32::max_value(), + _ => false, + } +} diff --git a/wasmtime-interface-types/src/value.rs b/wasmtime-interface-types/src/value.rs new file mode 100644 index 0000000000..3fd9f7c084 --- /dev/null +++ b/wasmtime-interface-types/src/value.rs @@ -0,0 +1,54 @@ +use std::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) + } + } + )*) +} + +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), + } + } +}