mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-29 03:04:38 +08:00
Merge pull request #2141 from RustPython/coolreader18/pynativefunc-rework
Just use normal Box for PyNativeFunc
This commit is contained in:
14 files changed
+75
-105
No files matched your search
@@ -6,7 +6,7 @@ on:
|
||||
name: CI
|
||||
|
||||
env:
|
||||
CARGO_ARGS: --all --features ssl
|
||||
CARGO_ARGS: --features ssl
|
||||
|
||||
jobs:
|
||||
rust_tests:
|
||||
@@ -112,6 +112,11 @@ jobs:
|
||||
with:
|
||||
command: clippy
|
||||
args: ${{ env.CARGO_ARGS }} -- -Dwarnings
|
||||
- name: run clippy on wasm
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --manifest-path=wasm/lib/Cargo.toml -- -Dwarnings
|
||||
- uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
Generated
-18
@@ -552,15 +552,6 @@ version = "0.1.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b980f2816d6ee8673b6517b52cb0e808a180efc92e5c19d02cdda79066703ef"
|
||||
|
||||
[[package]]
|
||||
name = "generational-arena"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e1d3b771574f62d0548cee0ad9057857e9fc25d7a3335f140c84f6acd0bf601"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.12.3"
|
||||
@@ -1398,7 +1389,6 @@ dependencies = [
|
||||
"flamer",
|
||||
"flate2",
|
||||
"foreign-types-shared",
|
||||
"generational-arena",
|
||||
"gethostname",
|
||||
"getrandom",
|
||||
"hex",
|
||||
@@ -1444,7 +1434,6 @@ dependencies = [
|
||||
"sha-1",
|
||||
"sha2",
|
||||
"sha3",
|
||||
"smallbox",
|
||||
"socket2",
|
||||
"statrs",
|
||||
"thread_local",
|
||||
@@ -1470,7 +1459,6 @@ version = "0.1.2"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures",
|
||||
"generational-arena",
|
||||
"js-sys",
|
||||
"parking_lot",
|
||||
"rustpython-compiler",
|
||||
@@ -1625,12 +1613,6 @@ version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa8f3741c7372e75519bd9346068370c9cdaabcc1f9599cbcf2a2719352286b7"
|
||||
|
||||
[[package]]
|
||||
name = "smallbox"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa0eec6425219a55511a35afac416a2eecc62f5fa1bec14f9b97de0caa7daebe"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.4.1"
|
||||
|
||||
@@ -272,7 +272,7 @@ fn extract_impl_items(mut items: Vec<ItemIdent>) -> Result<TokenStream2, Diagnos
|
||||
let transform = if vec!["new", "call"].contains(&slot_ident.to_string().as_str()) {
|
||||
quote! { ::rustpython_vm::function::IntoPyNativeFunc::into_func }
|
||||
} else {
|
||||
quote! { ::rustpython_vm::__exports::smallbox! }
|
||||
quote! { ::std::boxed::Box::new }
|
||||
};
|
||||
let into_func = quote_spanned! {item_ident.span()=>
|
||||
#transform(Self::#item_ident)
|
||||
|
||||
@@ -69,10 +69,8 @@ is-macro = "0.1"
|
||||
result-like = "^0.2.1"
|
||||
foreign-types-shared = "0.1"
|
||||
num_enum = "0.4"
|
||||
smallbox = "0.8"
|
||||
bstr = "0.2.12"
|
||||
crossbeam-utils = "0.7"
|
||||
generational-arena = "0.2"
|
||||
parking_lot = "0.11"
|
||||
thread_local = "1.0"
|
||||
cfg-if = "0.1.10"
|
||||
|
||||
+36
-33
@@ -4,13 +4,13 @@ use std::ops::RangeInclusive;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use result_like::impl_option_like;
|
||||
use smallbox::{smallbox, space::S1, SmallBox};
|
||||
|
||||
use crate::exceptions::PyBaseExceptionRef;
|
||||
use crate::obj::objtuple::PyTupleRef;
|
||||
use crate::obj::objtype::{isinstance, PyClassRef};
|
||||
use crate::pyobject::{
|
||||
BorrowValue, IntoPyResult, PyObjectRef, PyRef, PyResult, PyValue, TryFromObject, TypeProtocol,
|
||||
BorrowValue, IntoPyResult, PyObjectRef, PyRef, PyResult, PyThreadingConstraint, PyValue,
|
||||
TryFromObject, TypeProtocol,
|
||||
};
|
||||
use crate::vm::VirtualMachine;
|
||||
|
||||
@@ -470,12 +470,8 @@ tuple_from_py_func_args!(A, B, C, D);
|
||||
tuple_from_py_func_args!(A, B, C, D, E);
|
||||
tuple_from_py_func_args!(A, B, C, D, E, F);
|
||||
|
||||
/// A container that can hold a `dyn Fn*` trait object, but doesn't allocate if it's only a fn() pointer
|
||||
pub type FunctionBox<T> = SmallBox<T, S1>;
|
||||
|
||||
/// A built-in Python function.
|
||||
pub type PyNativeFunc =
|
||||
FunctionBox<dyn Fn(&VirtualMachine, PyFuncArgs) -> PyResult + 'static + Send + Sync>;
|
||||
pub type PyNativeFunc = Box<py_dyn_fn!(dyn Fn(&VirtualMachine, PyFuncArgs) -> PyResult)>;
|
||||
|
||||
/// Implemented by types that are or can generate built-in functions.
|
||||
///
|
||||
@@ -491,16 +487,19 @@ pub type PyNativeFunc =
|
||||
///
|
||||
/// A bare `PyNativeFunc` also implements this trait, allowing the above to be
|
||||
/// done manually, for rare situations that don't fit into this model.
|
||||
pub trait IntoPyNativeFunc<T, R, VM> {
|
||||
fn into_func(self) -> PyNativeFunc;
|
||||
pub trait IntoPyNativeFunc<T, R, VM>: Sized + PyThreadingConstraint + 'static {
|
||||
fn call(&self, vm: &VirtualMachine, args: PyFuncArgs) -> PyResult;
|
||||
fn into_func(self) -> PyNativeFunc {
|
||||
Box::new(move |vm: &VirtualMachine, args| self.call(vm, args))
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> IntoPyNativeFunc<PyFuncArgs, PyResult, VirtualMachine> for F
|
||||
where
|
||||
F: Fn(&VirtualMachine, PyFuncArgs) -> PyResult + 'static + Send + Sync,
|
||||
F: Fn(&VirtualMachine, PyFuncArgs) -> PyResult + PyThreadingConstraint + 'static,
|
||||
{
|
||||
fn into_func(self) -> PyNativeFunc {
|
||||
smallbox!(self)
|
||||
fn call(&self, vm: &VirtualMachine, args: PyFuncArgs) -> PyResult {
|
||||
(self)(vm, args)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -515,55 +514,55 @@ macro_rules! into_py_native_func_tuple {
|
||||
($(($n:tt, $T:ident)),*) => {
|
||||
impl<F, $($T,)* R> IntoPyNativeFunc<($(OwnedParam<$T>,)*), R, VirtualMachine> for F
|
||||
where
|
||||
F: Fn($($T,)* &VirtualMachine) -> R + 'static + Send + Sync,
|
||||
F: Fn($($T,)* &VirtualMachine) -> R + PyThreadingConstraint + 'static,
|
||||
$($T: FromArgs,)*
|
||||
R: IntoPyResult,
|
||||
{
|
||||
fn into_func(self) -> PyNativeFunc {
|
||||
smallbox!(move |vm: &VirtualMachine, args: PyFuncArgs| {
|
||||
let ($($n,)*) = args.bind::<($($T,)*)>(vm)?;
|
||||
fn call(&self, vm: &VirtualMachine, args: PyFuncArgs) -> PyResult {
|
||||
let ($($n,)*) = args.bind::<($($T,)*)>(vm)?;
|
||||
|
||||
(self)($($n,)* vm).into_pyresult(vm)
|
||||
})
|
||||
(self)($($n,)* vm).into_pyresult(vm)
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, S, $($T,)* R> IntoPyNativeFunc<(RefParam<S>, $(OwnedParam<$T>,)*), R, VirtualMachine> for F
|
||||
where
|
||||
F: Fn(&S, $($T,)* &VirtualMachine) -> R + 'static + Send + Sync,
|
||||
F: Fn(&S, $($T,)* &VirtualMachine) -> R + PyThreadingConstraint + 'static,
|
||||
S: PyValue,
|
||||
$($T: FromArgs,)*
|
||||
R: IntoPyResult,
|
||||
{
|
||||
fn into_func(self) -> PyNativeFunc {
|
||||
smallbox!(move |vm: &VirtualMachine, args: PyFuncArgs| {
|
||||
let (zelf, $($n,)*) = args.bind::<(PyRef<S>, $($T,)*)>(vm)?;
|
||||
fn call(&self, vm: &VirtualMachine, args: PyFuncArgs) -> PyResult {
|
||||
let (zelf, $($n,)*) = args.bind::<(PyRef<S>, $($T,)*)>(vm)?;
|
||||
|
||||
(self)(&zelf, $($n,)* vm).into_pyresult(vm)
|
||||
})
|
||||
(self)(&zelf, $($n,)* vm).into_pyresult(vm)
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, $($T,)* R> IntoPyNativeFunc<($(OwnedParam<$T>,)*), R, ()> for F
|
||||
where
|
||||
F: Fn($($T,)*) -> R + 'static + Send + Sync,
|
||||
F: Fn($($T,)*) -> R + PyThreadingConstraint + 'static,
|
||||
$($T: FromArgs,)*
|
||||
R: IntoPyResult,
|
||||
{
|
||||
fn into_func(self) -> PyNativeFunc {
|
||||
IntoPyNativeFunc::into_func(move |$($n,)* _vm: &VirtualMachine| (self)($($n,)*))
|
||||
fn call(&self, vm: &VirtualMachine, args: PyFuncArgs) -> PyResult {
|
||||
let ($($n,)*) = args.bind::<($($T,)*)>(vm)?;
|
||||
|
||||
(self)($($n,)*).into_pyresult(vm)
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, S, $($T,)* R> IntoPyNativeFunc<(RefParam<S>, $(OwnedParam<$T>,)*), R, ()> for F
|
||||
where
|
||||
F: Fn(&S, $($T,)*) -> R + 'static + Send + Sync,
|
||||
F: Fn(&S, $($T,)*) -> R + PyThreadingConstraint + 'static,
|
||||
S: PyValue,
|
||||
$($T: FromArgs,)*
|
||||
R: IntoPyResult,
|
||||
{
|
||||
fn into_func(self) -> PyNativeFunc {
|
||||
IntoPyNativeFunc::into_func(move |zelf: &S, $($n,)* _vm: &VirtualMachine| (self)(zelf, $($n,)*))
|
||||
fn call(&self, vm: &VirtualMachine, args: PyFuncArgs) -> PyResult {
|
||||
let (zelf, $($n,)*) = args.bind::<(PyRef<S>, $($T,)*)>(vm)?;
|
||||
|
||||
(self)(&zelf, $($n,)*).into_pyresult(vm)
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -633,12 +632,16 @@ where
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_functionbox_noalloc() {
|
||||
fn test_intonativefunc_noalloc() {
|
||||
let check_zst = |f: PyNativeFunc| assert_eq!(std::mem::size_of_val(f.as_ref()), 0);
|
||||
fn py_func(_b: bool, _vm: &crate::VirtualMachine) -> i32 {
|
||||
1
|
||||
}
|
||||
let f = super::IntoPyNativeFunc::into_func(py_func);
|
||||
assert!(!f.is_heap());
|
||||
check_zst(py_func.into_func());
|
||||
let empty_closure = || "foo".to_owned();
|
||||
check_zst(empty_closure.into_func());
|
||||
}
|
||||
}
|
||||
@@ -73,8 +73,3 @@ mod vm;
|
||||
pub use self::vm::{InitParameter, PySettings, VirtualMachine};
|
||||
pub use rustpython_bytecode::*;
|
||||
pub use rustpython_common as common;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub mod __exports {
|
||||
pub use smallbox::smallbox;
|
||||
}
|
||||
@@ -240,3 +240,21 @@ macro_rules! named_function {
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
// can't use PyThreadingConstraint for stuff like this since it's not an auto trait, and
|
||||
// therefore we can't add it ad-hoc to a trait object
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "threading")] {
|
||||
macro_rules! py_dyn_fn {
|
||||
(dyn Fn($($arg:ty),*$(,)*) -> $ret:ty) => {
|
||||
dyn Fn($($arg),*) -> $ret + Send + Sync + 'static
|
||||
};
|
||||
}
|
||||
} else {
|
||||
macro_rules! py_dyn_fn {
|
||||
(dyn Fn($($arg:ty),*$(,)*) -> $ret:ty) => {
|
||||
dyn Fn($($arg),*) -> $ret + 'static
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
*/
|
||||
use super::objtype::PyClassRef;
|
||||
use crate::function::{FunctionBox, OptionalArg, OwnedParam, RefParam};
|
||||
use crate::function::{OptionalArg, OwnedParam, RefParam};
|
||||
use crate::pyobject::{
|
||||
IntoPyResult, PyClassImpl, PyContext, PyObjectRef, PyRef, PyResult, PyValue, TryFromObject,
|
||||
TypeProtocol,
|
||||
@@ -10,9 +10,9 @@ use crate::pyobject::{
|
||||
use crate::slots::SlotDescriptor;
|
||||
use crate::vm::VirtualMachine;
|
||||
|
||||
pub type PyGetterFunc = FunctionBox<dyn Fn(&VirtualMachine, PyObjectRef) -> PyResult + Send + Sync>;
|
||||
pub type PyGetterFunc = Box<py_dyn_fn!(dyn Fn(&VirtualMachine, PyObjectRef) -> PyResult)>;
|
||||
pub type PySetterFunc =
|
||||
FunctionBox<dyn Fn(&VirtualMachine, PyObjectRef, PyObjectRef) -> PyResult<()> + Send + Sync>;
|
||||
Box<py_dyn_fn!(dyn Fn(&VirtualMachine, PyObjectRef, PyObjectRef) -> PyResult<()>)>;
|
||||
|
||||
pub trait IntoPyGetterFunc<T> {
|
||||
fn into_getter(self) -> PyGetterFunc;
|
||||
@@ -25,7 +25,7 @@ where
|
||||
R: IntoPyResult,
|
||||
{
|
||||
fn into_getter(self) -> PyGetterFunc {
|
||||
smallbox::smallbox!(move |vm: &VirtualMachine, obj| {
|
||||
Box::new(move |vm: &VirtualMachine, obj| {
|
||||
let obj = T::try_from_object(vm, obj)?;
|
||||
(self)(obj, vm).into_pyresult(vm)
|
||||
})
|
||||
@@ -39,7 +39,7 @@ where
|
||||
R: IntoPyResult,
|
||||
{
|
||||
fn into_getter(self) -> PyGetterFunc {
|
||||
smallbox::smallbox!(move |vm: &VirtualMachine, obj| {
|
||||
Box::new(move |vm: &VirtualMachine, obj| {
|
||||
let zelf = PyRef::<S>::try_from_object(vm, obj)?;
|
||||
(self)(&zelf, vm).into_pyresult(vm)
|
||||
})
|
||||
@@ -96,7 +96,7 @@ where
|
||||
R: IntoPyNoResult,
|
||||
{
|
||||
fn into_setter(self) -> PySetterFunc {
|
||||
smallbox::smallbox!(move |vm: &VirtualMachine, obj, value| {
|
||||
Box::new(move |vm: &VirtualMachine, obj, value| {
|
||||
let obj = T::try_from_object(vm, obj)?;
|
||||
let value = V::try_from_object(vm, value)?;
|
||||
(self)(obj, value, vm).into_noresult()
|
||||
@@ -112,7 +112,7 @@ where
|
||||
R: IntoPyNoResult,
|
||||
{
|
||||
fn into_setter(self) -> PySetterFunc {
|
||||
smallbox::smallbox!(move |vm: &VirtualMachine, obj, value| {
|
||||
Box::new(move |vm: &VirtualMachine, obj, value| {
|
||||
let zelf = PyRef::<S>::try_from_object(vm, obj)?;
|
||||
let value = V::try_from_object(vm, value)?;
|
||||
(self)(&zelf, value, vm).into_noresult()
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
use crate::function::{FunctionBox, OptionalArg, PyFuncArgs, PyNativeFunc};
|
||||
use crate::function::{OptionalArg, PyFuncArgs, PyNativeFunc};
|
||||
use crate::pyobject::{IdProtocol, PyObjectRef, PyRef, PyResult, PyValue, TryFromObject};
|
||||
use crate::VirtualMachine;
|
||||
|
||||
@@ -54,7 +54,7 @@ pub trait SlotCall: PyValue {
|
||||
fn call(&self, args: PyFuncArgs, vm: &VirtualMachine) -> PyResult;
|
||||
}
|
||||
|
||||
pub type PyDescrGetFunc = FunctionBox<
|
||||
pub type PyDescrGetFunc = Box<
|
||||
dyn Fn(&VirtualMachine, PyObjectRef, Option<PyObjectRef>, OptionalArg<PyObjectRef>) -> PyResult
|
||||
+ Send
|
||||
+ Sync,
|
||||
|
||||
@@ -68,7 +68,7 @@ mod winreg;
|
||||
#[cfg(not(any(target_arch = "wasm32", target_os = "redox")))]
|
||||
mod zlib;
|
||||
|
||||
pub type StdlibInitFunc = Box<dyn Fn(&VirtualMachine) -> PyObjectRef + Send + Sync>;
|
||||
pub type StdlibInitFunc = Box<py_dyn_fn!(dyn Fn(&VirtualMachine) -> PyObjectRef)>;
|
||||
|
||||
pub fn get_module_inits() -> HashMap<String, StdlibInitFunc> {
|
||||
#[allow(unused_mut)]
|
||||
|
||||
@@ -26,7 +26,6 @@ serde-wasm-bindgen = "0.1"
|
||||
serde = "1.0"
|
||||
js-sys = "0.3"
|
||||
futures = "0.1"
|
||||
generational-arena = "0.2"
|
||||
# make parking_lot use wasm-bingden for instant
|
||||
parking_lot = { version = "0.11", features = ["wasm-bindgen"] }
|
||||
|
||||
|
||||
+1
-30
@@ -1,6 +1,3 @@
|
||||
use generational_arena::Arena;
|
||||
use std::cell::RefCell;
|
||||
|
||||
use js_sys::{Array, ArrayBuffer, Object, Promise, Reflect, SyntaxError, Uint8Array};
|
||||
use wasm_bindgen::{closure::Closure, prelude::*, JsCast};
|
||||
|
||||
@@ -17,31 +14,6 @@ use rustpython_vm::{exceptions, py_serde};
|
||||
use crate::browser_module;
|
||||
use crate::vm_class::{stored_vm_from_wasm, WASMVirtualMachine};
|
||||
|
||||
// Currently WASM do not support multithreading. We should change this once it is enabled.
|
||||
thread_local!(static JS_HANDLES: RefCell<Arena<JsValue>> = RefCell::new(Arena::new()));
|
||||
|
||||
pub struct JsHandle(generational_arena::Index);
|
||||
impl JsHandle {
|
||||
pub fn new(js: JsValue) -> Self {
|
||||
let idx = JS_HANDLES.with(|arena| arena.borrow_mut().insert(js));
|
||||
JsHandle(idx)
|
||||
}
|
||||
pub fn get(&self) -> JsValue {
|
||||
JS_HANDLES.with(|arena| {
|
||||
arena
|
||||
.borrow()
|
||||
.get(self.0)
|
||||
.expect("index was removed")
|
||||
.clone()
|
||||
})
|
||||
}
|
||||
}
|
||||
impl Drop for JsHandle {
|
||||
fn drop(&mut self) {
|
||||
JS_HANDLES.with(|arena| arena.borrow_mut().remove(self.0));
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(inline_js = r"
|
||||
export class PyError extends Error {
|
||||
constructor(info) {
|
||||
@@ -223,7 +195,7 @@ pub fn js_to_py(vm: &VirtualMachine, js_val: JsValue) -> PyObjectRef {
|
||||
dict.into_object()
|
||||
}
|
||||
} else if js_val.is_function() {
|
||||
let js_handle = JsHandle::new(js_val);
|
||||
let func = js_sys::Function::from(js_val);
|
||||
vm.ctx
|
||||
.new_method(move |vm: &VirtualMachine, args: PyFuncArgs| -> PyResult {
|
||||
let this = Object::new();
|
||||
@@ -235,7 +207,6 @@ pub fn js_to_py(vm: &VirtualMachine, js_val: JsValue) -> PyObjectRef {
|
||||
for v in args.args {
|
||||
js_args.push(&py_to_js(vm, v));
|
||||
}
|
||||
let func = js_sys::Function::from(js_handle.get());
|
||||
func.apply(&this, &js_args)
|
||||
.map(|val| js_to_py(vm, val))
|
||||
.map_err(|err| js_err_to_py_err(vm, &err))
|
||||
|
||||
@@ -12,7 +12,7 @@ use rustpython_vm::scope::{NameProtocol, Scope};
|
||||
use rustpython_vm::{InitParameter, PySettings, VirtualMachine};
|
||||
|
||||
use crate::browser_module::setup_browser_module;
|
||||
use crate::convert::{self, JsHandle, PyResultExt};
|
||||
use crate::convert::{self, PyResultExt};
|
||||
use crate::js_module;
|
||||
use crate::wasm_builtins;
|
||||
use rustpython_compiler::mode::Mode;
|
||||
@@ -227,9 +227,8 @@ impl WASMVirtualMachine {
|
||||
_ => return Err(error()),
|
||||
}
|
||||
} else if stdout.is_function() {
|
||||
let func_handle = JsHandle::new(stdout);
|
||||
let func = js_sys::Function::from(stdout);
|
||||
make_stdout_object(vm, move |data, vm| {
|
||||
let func = js_sys::Function::from(func_handle.get());
|
||||
func.call1(&JsValue::UNDEFINED, &data.into())
|
||||
.map_err(|err| convert::js_py_typeerror(vm, err))?;
|
||||
Ok(())
|
||||
|
||||
@@ -21,7 +21,7 @@ pub fn sys_stdout_write_console(data: &str, _vm: &VirtualMachine) -> PyResult<()
|
||||
|
||||
pub fn make_stdout_object(
|
||||
vm: &VirtualMachine,
|
||||
write_f: impl Fn(&str, &VirtualMachine) -> PyResult<()> + Send + Sync + 'static,
|
||||
write_f: impl Fn(&str, &VirtualMachine) -> PyResult<()> + 'static,
|
||||
) -> PyObjectRef {
|
||||
let ctx = &vm.ctx;
|
||||
let write_method = ctx.new_method(
|
||||
|
||||
Reference in new issue
Block a user