Add WAMR API bindings in Python (#1959)

Before adding the new bindings:
1. Moved wasm-c-api in a subfolder wasmcapi in the package.
2. Adapted the tests to be able to run in this new structure.

New:
1. Added the WAMR API in another folder wamrapi in the same level as wasm-c-api.
2. Created an OOP proposal.
3. Added an example using this proposal.
This commit is contained in:
tonibofarull 2023-02-16 08:21:28 +01:00 committed by GitHub
parent f60c3c6111
commit 3cc132e8fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 2054 additions and 28 deletions

View File

@ -0,0 +1 @@
include src/wamr/libs/*

View File

@ -1,31 +1,34 @@
# wamr-python
The WAMR Python package contains a set of high-level bindings for WAMR API and WASM-C-API.
## Installation
### Installing from the source code
Installing from local source tree is in _development mode_. The package appears to be installed but still is editable from the source tree.
To Install from local source tree in _development mode_ run the following command,
```bash
$ python -m pip install -e /path/to/wamr-root/binding/python
python -m pip install -e .
```
In this mode the package appears to be installed but still is editable from the source tree.
## Usage
```python
import wamr.ffi as ffi
From the same package you can use two set of APIs.
To use the WAMR API you can import the symbols as follows,
```py
from wamr.wamrapi.wamr import Engine, Module, Instance, ExecEnv
```
### Preparation
In the order hand, to use the WASM-C-API,
The binding will load the shared library _libiwasm.so_ from the WAMR repo. So before running the binding, you need to build the library yourself.
```py
import wamr.wasmcapi.ffi as ffi
```
The default compile options are good enough.
For more information:
Please be aware that `wasm_frame_xxx` and `wasm_trap_xxx` only work well when enabling `WAMR_BUILD_DUMP_CALL_STACK`.
### Examples
There is a [simple example](./samples/hello_procedural.py) to show how to use bindings. Actually, the python binding follows C-APIs. There it should be easy if be familiar with _programming with wasm-c-api_.
Unit test cases under _./tests_ could be another but more complete references.
* [WAMR API](./wamr_api)
* [WASM-C-API](./wasm_c_api)

View File

@ -8,7 +8,28 @@
# pylint: disable=missing-function-docstring
# pylint: disable=missing-module-docstring
from setuptools import setup, find_packages
import pathlib
from setuptools import setup
from setuptools.command.develop import develop
from setuptools.command.install import install
from subprocess import check_call
def build_library():
cur_path = pathlib.Path(__file__).parent
check_call(f"{cur_path}/utils/create_lib.sh".split())
class PreDevelopCommand(develop):
"""Pre-installation for development mode."""
def run(self):
build_library()
develop.run(self)
class PreInstallCommand(install):
"""Pre-installation for installation mode."""
def run(self):
build_library()
install.run(self)
with open("README.md") as f:
@ -24,7 +45,11 @@ setup(
long_description=readme,
author="The WAMR Project Developers",
author_email="hello@bytecodealliance.org",
url="https://github.com/bytecodealliance/wamr-python",
url="https://github.com/bytecodealliance/wasm-micro-runtime",
license=license,
packages=["wamr"],
include_package_data=True,
cmdclass={
'develop': PreDevelopCommand,
'install': PreInstallCommand,
},
)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,123 @@
# Copyright (C) 2019 Intel Corporation. All rights reserved.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
from ctypes import Array
from ctypes import c_char
from ctypes import c_uint
from ctypes import c_uint8
from ctypes import c_void_p
from ctypes import cast
from ctypes import create_string_buffer
from ctypes import POINTER
from ctypes import pointer
from wamr.wamrapi.iwasm import Alloc_With_Pool
from wamr.wamrapi.iwasm import RuntimeInitArgs
from wamr.wamrapi.iwasm import wasm_exec_env_t
from wamr.wamrapi.iwasm import wasm_function_inst_t
from wamr.wamrapi.iwasm import wasm_module_inst_t
from wamr.wamrapi.iwasm import wasm_module_t
from wamr.wamrapi.iwasm import wasm_runtime_call_wasm
from wamr.wamrapi.iwasm import wasm_runtime_create_exec_env
from wamr.wamrapi.iwasm import wasm_runtime_deinstantiate
from wamr.wamrapi.iwasm import wasm_runtime_destroy
from wamr.wamrapi.iwasm import wasm_runtime_destroy_exec_env
from wamr.wamrapi.iwasm import wasm_runtime_full_init
from wamr.wamrapi.iwasm import wasm_runtime_instantiate
from wamr.wamrapi.iwasm import wasm_runtime_load
from wamr.wamrapi.iwasm import wasm_runtime_lookup_function
from wamr.wamrapi.iwasm import wasm_runtime_unload
class Engine:
def __init__(self):
self.init_args = self._get_init_args()
wasm_runtime_full_init(pointer(self.init_args))
def __del__(self):
print("deleting Engine")
wasm_runtime_destroy()
def _get_init_args(self, heap_size: int = 1024 * 512) -> RuntimeInitArgs:
init_args = RuntimeInitArgs()
init_args.mem_alloc_type = Alloc_With_Pool
init_args.mem_alloc_option.pool.heap_buf = cast(
(c_char * heap_size)(), c_void_p
)
init_args.mem_alloc_option.pool.heap_size = heap_size
return init_args
class Module:
__create_key = object()
@classmethod
def from_file(cls, engine: Engine, fp: str) -> "Module":
return Module(cls.__create_key, engine, fp)
def __init__(self, create_key: object, engine: Engine, fp: str) -> None:
assert (
create_key == Module.__create_key
), "Module objects must be created using Module.from_file"
self.engine = engine
self.module, self.file_data = self._create_module(fp)
def __del__(self):
print("deleting Module")
wasm_runtime_unload(self.module)
def _create_module(self, fp: str) -> tuple[wasm_module_t, Array[c_uint]]:
with open(fp, "rb") as f:
data = f.read()
data = (c_uint8 * len(data))(*data)
error_buf = create_string_buffer(128)
module = wasm_runtime_load(data, len(data), error_buf, len(error_buf))
if not module:
raise Exception("Error while creating module")
return module, data
class Instance:
def __init__(self, module: Module, stack_size: int = 65536, heap_size: int = 16384):
self.module = module
self.module_inst = self._create_module_inst(module, stack_size, heap_size)
def __del__(self):
print("deleting Instance")
wasm_runtime_deinstantiate(self.module_inst)
def lookup_function(self, name: str):
func = wasm_runtime_lookup_function(self.module_inst, name, None)
if not func:
raise Exception("Error while looking-up function")
return func
def _create_module_inst(self, module: Module, stack_size: int, heap_size: int) -> wasm_module_inst_t:
error_buf = create_string_buffer(128)
module_inst = wasm_runtime_instantiate(
module.module, stack_size, heap_size, error_buf, len(error_buf)
)
if not module_inst:
raise Exception("Error while creating module instance")
return module_inst
class ExecEnv:
def __init__(self, module_inst: Instance, stack_size: int = 65536):
self.module_inst = module_inst
self.exec_env = self._create_exec_env(module_inst, stack_size)
def __del__(self):
print("deleting ExecEnv")
wasm_runtime_destroy_exec_env(self.exec_env)
def call(self, func: wasm_function_inst_t, argc: int, argv: "POINTER[c_uint]"):
if not wasm_runtime_call_wasm(self.exec_env, func, argc, argv):
raise Exception("Error while calling function")
def _create_exec_env(self, module_inst: Instance, stack_size: int) -> wasm_exec_env_t:
exec_env = wasm_runtime_create_exec_env(module_inst.module_inst, stack_size)
if not exec_env:
raise Exception("Error while creating execution environment")
return exec_env

View File

@ -36,8 +36,8 @@ current_file = Path(__file__)
if current_file.is_symlink():
current_file = Path(os.readlink(current_file))
current_dir = current_file.parent.resolve()
root_dir = current_dir.parent.parent.parent.parent.resolve()
wamr_dir = root_dir.joinpath("wasm-micro-runtime").resolve()
root_dir = current_dir.parents[4].resolve()
wamr_dir = root_dir.resolve()
if not wamr_dir.exists():
raise RuntimeError(f"not found the repo of wasm-micro-runtime under {root_dir}")

View File

@ -0,0 +1,17 @@
#!/bin/sh
# Copyright (C) 2019 Intel Corporation. All rights reserved.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
CUR_DIR=$(cd $(dirname $0) && pwd -P)
ROOT_DIR=${CUR_DIR}/../../..
WAMR_BUILD_PLATFORM=${WAMR_BUILD_PLATFORM:-"linux"}
cd ${ROOT_DIR}/product-mini/platforms/${WAMR_BUILD_PLATFORM}
mkdir -p build && cd build
cmake ..
make -j
cp libiwasm.so ${CUR_DIR}/../src/wamr/libs

View File

@ -0,0 +1,25 @@
# WARM API
## Examples
Copy in `language-bindings/python/wamr/libs` the library `libiwasm` generated from `product-mini/platforms`.
There is a [simple example](./samples/main.py) to show how to use bindings.
```
python samples/main.py
```
## Update WAMR API bindings
Install requirements,
```
pip install -r requirements.txt
```
Run the following command,
```sh
ctypesgen ../../../../core/iwasm/include/wasm_export.h -l ../libs/libiwasm.so -o iwasm.py
```

View File

@ -0,0 +1 @@
ctypesgen==1.1.1

View File

@ -0,0 +1,11 @@
#!/bin/sh
# Copyright (C) 2019 Intel Corporation. All rights reserved.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
/opt/wasi-sdk/bin/clang \
-O0 -z stack-size=4096 -Wl,--initial-memory=65536 \
-Wl,--strip-all,--no-entry -nostdlib \
-Wl,--export=sum\
-Wl,--allow-undefined \
-o test.wasm sum.c

View File

@ -0,0 +1,22 @@
# Copyright (C) 2019 Intel Corporation. All rights reserved.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
from wamr.wamrapi.wamr import Engine, Module, Instance, ExecEnv
from ctypes import c_uint
import pathlib
def main():
engine = Engine()
module = Module.from_file(engine, pathlib.Path(__file__).parent / "sum.wasm")
module_inst = Instance(module)
exec_env = ExecEnv(module_inst)
func = module_inst.lookup_function("sum")
argv = (c_uint * 2)(*[10, 11])
exec_env.call(func, len(argv), argv)
print(argv[0])
if __name__ == "__main__":
main()

View File

@ -0,0 +1,12 @@
/*
* Copyright (C) 2019 Intel Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
*/
#include <stdio.h>
int
sum(int a, int b)
{
return a + b;
}

View File

@ -0,0 +1,7 @@
# WASM-C-API
## Examples
There is a [simple example](./samples/hello_procedural.py) to show how to use bindings. Actually, the python binding follows C-APIs. There it should be easy if be familiar with _programming with wasm-c-api_.
Unit test cases under _./tests_ could be another but more complete references.

View File

@ -5,7 +5,7 @@
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#
import ctypes
import wamr.ffi as ffi
import wamr.wasmcapi.ffi as ffi
WAMS_BINARY_CONTENT = (
b"\x00asm\x01\x00\x00\x00\x01\x84\x80\x80\x80\x00\x01`\x00\x00\x02\x8a\x80"

View File

@ -12,7 +12,7 @@ import ctypes as c
import math
import unittest
import wamr.ffi as ffi
import wamr.wasmcapi.ffi as ffi
# It is a module likes:

View File

@ -12,7 +12,7 @@ import ctypes as c
import unittest
from venv import create
from wamr.ffi import *
from wamr.wasmcapi.ffi import *
# It is a module likes:
# (module

View File

@ -21,7 +21,7 @@ import sys
from pycparser import c_ast, parse_file
WASM_C_API_HEADER = "core/iwasm/include/wasm_c_api.h"
BINDING_PATH = "wamr/binding.py"
BINDING_PATH = "language-bindings/python/wamr/wasmcapi/binding.py"
# 4 spaces as default indent
INDENT = " "
@ -314,7 +314,7 @@ class Visitor(c_ast.NodeVisitor):
def preflight_check(workspace):
wamr_repo = workspace.joinpath("wasm-micro-runtime")
wamr_repo = workspace
file_check_list = [
wamr_repo.exists(),
wamr_repo.joinpath(WASM_C_API_HEADER).exists(),
@ -369,12 +369,12 @@ def main():
current_file = pathlib.Path(os.readlink(current_file))
current_dir = current_file.parent.resolve()
root_dir = current_dir.joinpath("..").resolve()
root_dir = current_dir.joinpath("../../../..").resolve()
if not preflight_check(root_dir):
return False
wamr_repo = root_dir.joinpath("wasm-micro-runtime")
wamr_repo = root_dir
binding_file_path = root_dir.joinpath(BINDING_PATH)
with open(binding_file_path, "wt", encoding="utf-8") as binding_file:
binding_file.write(do_parse(wamr_repo))