代码在main中正常运行,抽取为函数时触发可变借用冲突错误
使用deno_core crate执行一段小型JavaScript代码片段,获取结果对象并取出其中的test属性,将其作为函数执行。以下代码全部写在main函数中时可正常运行,但尝试将相关逻辑抽取为独立函数时编译失败。请问如何避免此类情况?为什么在main中可以运行?是否可以将这段逻辑抽取为一个函数,正确的做法是什么?
代码示例及编译错误
use deno_core::v8; use deno_core::JsRuntime; fn main() -> anyhow::Result<()> { let mut runtime = JsRuntime::new(Default::default()); let executed = runtime.execute_script("name", "({ test: (a, b) => { console.log(a + b) } })")?; let scope = &mut runtime.handle_scope(); let local_executed = v8::Local::new(scope, executed); let result_obj = v8::Local::<v8::Object>::try_from(local_executed)?; let param1 = v8::String::new(scope, "Hello").unwrap(); let param2 = v8::String::new(scope, "World").unwrap(); let args = vec![param1.into(), param2.into()]; let fn_name: v8::Local<v8::String> = v8::String::new(scope, "my_function").unwrap(); result_obj.get(scope, fn_name.into()) .map(|value| v8::Local::<v8::Function>::try_from(value)) .unwrap()? .call(scope, local_executed, &args); // 若调用get_fn并执行返回的v8::Function,编译失败: // // get_fn(scope, result_obj).call(scope, local_executed, &args); // ----- ---- ^^^^^ 第二次可变借用发生在此处 // | | // | 第一次借用后续被call使用 // 第一次可变借用发生在此处 // // error[E0499]: cannot borrow `*scope` as mutable more than once at a time Ok(()) } fn get_fn<'a>(scope: &'a mut v8::HandleScope, object: v8::Local<'a, v8::Object>) -> v8::Local<'a, v8::Function> { let fn_name = v8::String::new(scope, "test").unwrap(); object.get(scope, fn_name.into()) .map(|value| v8::Local::<v8::Function>::try_from(value)) .unwrap() .unwrap() }
为什么main中可以正常运行
在main函数的链式调用result_obj.get(...).map(...).unwrap()?.call(...)里,Rust的借用检查器能识别出:get方法对scope的可变借用在map处理完成后就已结束,后续call方法的借用是全新的,不会产生重叠。链式调用的每一步都是临时值,借用的生命周期被限制在当前步骤,不会和后续步骤的借用冲突。
抽取函数时编译失败的原因
你写的get_fn函数将scope的可变借用和返回的v8::Local绑定到了同一个生命周期'a,这意味着从get_fn返回的Function会一直持有scope的可变借用,直到Function被销毁。当你调用get_fn(scope, result_obj).call(scope, ...)时,get_fn还未返回就已经把scope的可变借用占用,而call方法又需要再次借用scope,违反了Rust“同一时间只能有一个可变借用”的规则,导致编译错误。
正确抽取函数的做法
解决方法是将获取函数和执行函数拆分为两个独立步骤,让两次对scope的可变借用不会重叠。同时调整get_fn的错误处理,返回Result类型,更符合Rust最佳实践:
调整后的代码示例
use deno_core::v8; use deno_core::JsRuntime; fn main() -> anyhow::Result<()> { let mut runtime = JsRuntime::new(Default::default()); let executed = runtime.execute_script("name", "({ test: (a, b) => { console.log(a + b) } })")?; let scope = &mut runtime.handle_scope(); let local_executed = v8::Local::new(scope, executed); let result_obj = v8::Local::<v8::Object>::try_from(local_executed)?; let param1 = v8::String::new(scope, "Hello").unwrap(); let param2 = v8::String::new(scope, "World").unwrap(); let args = vec![param1.into(), param2.into()]; // 拆分两步:先获取函数,再执行,避免借用重叠 let test_fn = get_fn(scope, result_obj)?; test_fn.call(scope, local_executed, &args); Ok(()) } fn get_fn<'a>(scope: &'a mut v8::HandleScope, object: v8::Local<'a, v8::Object>) -> anyhow::Result<v8::Local<'a, v8::Function>> { let fn_name = v8::String::new(scope, "test").unwrap(); let value = object.get(scope, fn_name.into()) .ok_or_else(|| anyhow::anyhow!("Failed to get 'test' property"))?; let func = v8::Local::<v8::Function>::try_from(value) .map_err(|_| anyhow::anyhow!("Property 'test' is not a function"))?; Ok(func) }
核心原理
将操作拆分为两个独立语句后:
- 第一句
let test_fn = get_fn(scope, result_obj)?;完成对scope的可变借用,获取到函数后,借用立即结束。 - 第二句
test_fn.call(scope, ...)重新对scope进行可变借用,此时前一个借用已经释放,不会产生冲突。
内容的提问来源于stack exchange,提问作者almafa

