Skip to content

Commit ac9f7d1

Browse files
authored
Use cpython types in Rust functions that manipulate python objects (#10942)
### Problem We have a number of Rust functions in the `externs` module that manipulate Python values that make use of the `Value` type, which is our own reference-counted wrapper around the `PyObject` type provided by the `cpython` library. The `Value` type predates the use of `cpython` for interacting with Python code, and `PyObject` is a more flexible type than the previous type(s) used to interact with Python. Now it is often the case that we take `Value`s returned from one of the `externs` functions and immediately turn it back into a `PyObject`. This makes the API more confusing to reason about, and forces unnecessary and potentially costly type conversions. ### Solution Implement `AsRef<PyObject>` for `Value` so we can cheaply turn existing parameters of type `Value` into `&PyObject`s. Then, switch out as many function signatures as possible to work with `PyObject`s, using the `AsRef` and other existing conversion traits to turn `PyObject`s into `Value`s where necessary, and lets us save type conversions when we don't need them.
1 parent b4b0d04 commit ac9f7d1

File tree

7 files changed

+59
-58
lines changed

7 files changed

+59
-58
lines changed

src/rust/engine/src/core.rs

+13-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
use fnv::FnvHasher;
55

6+
use std::convert::AsRef;
67
use std::ops::Deref;
78
use std::sync::Arc;
89
use std::{fmt, hash};
@@ -264,6 +265,12 @@ impl Deref for Value {
264265
}
265266
}
266267

268+
impl AsRef<PyObject> for Value {
269+
fn as_ref(&self) -> &PyObject {
270+
&self.0
271+
}
272+
}
273+
267274
impl fmt::Debug for Value {
268275
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
269276
write!(f, "{}", externs::val_to_str(&self))
@@ -347,7 +354,12 @@ impl Failure {
347354
}
348355

349356
impl Failure {
350-
pub fn from_py_err(py: Python, mut py_err: PyErr) -> Failure {
357+
pub fn from_py_err(py_err: PyErr) -> Failure {
358+
let gil = Python::acquire_gil();
359+
let py = gil.python();
360+
Failure::from_py_err_with_gil(py, py_err)
361+
}
362+
pub fn from_py_err_with_gil(py: Python, mut py_err: PyErr) -> Failure {
351363
let val = Value::from(py_err.instance(py));
352364
let python_traceback = if let Some(tb) = py_err.ptraceback.as_ref() {
353365
let locals = PyDict::new(py);

src/rust/engine/src/externs/engine_aware.rs

+12-10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use crate::core::Value;
55
use crate::externs;
66
use crate::nodes::lift_directory_digest;
7+
use crate::Failure;
78
use crate::Types;
89

910
use cpython::{PyDict, PyObject, PyString, Python};
@@ -24,9 +25,9 @@ impl EngineAwareInformation for EngineAwareLevel {
2425
type MaybeOutput = Level;
2526

2627
fn retrieve(_types: &Types, value: &Value) -> Option<Level> {
27-
let new_level_val: Value = externs::call_method(&value, "level", &[]).ok()?;
28+
let new_level_val = externs::call_method(value.as_ref(), "level", &[]).ok()?;
2829
let new_level_val = externs::check_for_python_none(new_level_val)?;
29-
externs::val_to_log_level(&new_level_val).ok()
30+
externs::val_to_log_level(&new_level_val.into()).ok()
3031
}
3132
}
3233

@@ -36,9 +37,9 @@ impl EngineAwareInformation for Message {
3637
type MaybeOutput = String;
3738

3839
fn retrieve(_types: &Types, value: &Value) -> Option<String> {
39-
let msg_val: Value = externs::call_method(&value, "message", &[]).ok()?;
40+
let msg_val = externs::call_method(&value, "message", &[]).ok()?;
4041
let msg_val = externs::check_for_python_none(msg_val)?;
41-
Some(externs::val_to_str(&msg_val))
42+
Some(externs::val_to_str(&msg_val.into()))
4243
}
4344
}
4445

@@ -48,14 +49,15 @@ impl EngineAwareInformation for Artifacts {
4849
type MaybeOutput = Vec<(String, Digest)>;
4950

5051
fn retrieve(types: &Types, value: &Value) -> Option<Self::MaybeOutput> {
51-
let artifacts_val: Value = match externs::call_method(&value, "artifacts", &[]) {
52+
let artifacts_val = match externs::call_method(&value, "artifacts", &[]) {
5253
Ok(value) => value,
53-
Err(e) => {
54-
log::error!("Error calling `artifacts` method: {}", e);
54+
Err(py_err) => {
55+
let failure = Failure::from_py_err(py_err);
56+
log::error!("Error calling `artifacts` method: {}", failure);
5557
return None;
5658
}
5759
};
58-
let artifacts_val: Value = externs::check_for_python_none(artifacts_val)?;
60+
let artifacts_val = externs::check_for_python_none(artifacts_val)?;
5961
let gil = Python::acquire_gil();
6062
let py = gil.python();
6163
let artifacts_dict: &PyDict = artifacts_val.cast_as::<PyDict>(py).ok()?;
@@ -72,7 +74,7 @@ impl EngineAwareInformation for Artifacts {
7274
return None;
7375
}
7476
};
75-
let digest_value: PyObject = externs::getattr(&Value::new(value), "digest")
77+
let digest_value: PyObject = externs::getattr(&value, "digest")
7678
.map_err(|e| {
7779
log::error!("Error in EngineAware.artifacts() - no `digest` attr: {}", e);
7880
})
@@ -100,6 +102,6 @@ impl EngineAwareInformation for DebugHint {
100102
externs::call_method(&value, "debug_hint", &[])
101103
.ok()
102104
.and_then(externs::check_for_python_none)
103-
.map(|val| externs::val_to_str(&val))
105+
.map(|val| externs::val_to_str(&val.into()))
104106
}
105107
}

src/rust/engine/src/externs/interface.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -692,8 +692,9 @@ fn nailgun_server_create(
692692
stdout_fd,
693693
stderr_fd,
694694
];
695-
match externs::call(&runner, &runner_args) {
696-
Ok(exit_code_val) => {
695+
match externs::call_function(&runner, &runner_args) {
696+
Ok(exit_code) => {
697+
let exit_code_val: Value = exit_code.into();
697698
// TODO: We don't currently expose a "project_i32", but it will not be necessary with
698699
// https://github.com/pantsbuild/pants/pull/9593.
699700
nailgun::ExitCode(externs::val_to_str(&exit_code_val).parse().unwrap())

src/rust/engine/src/externs/mod.rs

+22-39
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ mod interface;
1515
mod interface_tests;
1616

1717
use std::collections::BTreeMap;
18+
use std::convert::AsRef;
1819
use std::convert::TryInto;
1920
use std::fmt;
2021
use std::sync::atomic;
@@ -97,14 +98,6 @@ pub fn type_for(py_type: PyType) -> TypeId {
9798
})
9899
}
99100

100-
pub fn acquire_key_for(val: Value) -> Result<Key, Failure> {
101-
key_for(val).map_err(|e| {
102-
let gil = Python::acquire_gil();
103-
let py = gil.python();
104-
Failure::from_py_err(py, e)
105-
})
106-
}
107-
108101
pub fn key_for(val: Value) -> Result<Key, PyErr> {
109102
with_interns_mut(|interns| {
110103
let gil = Python::acquire_gil();
@@ -174,7 +167,7 @@ pub fn store_bool(val: bool) -> Value {
174167
///
175168
/// Check if a Python object has the specified field.
176169
///
177-
pub fn hasattr(value: &Value, field: &str) -> bool {
170+
pub fn hasattr(value: &PyObject, field: &str) -> bool {
178171
let gil = Python::acquire_gil();
179172
let py = gil.python();
180173
value.hasattr(py, field).unwrap()
@@ -183,7 +176,7 @@ pub fn hasattr(value: &Value, field: &str) -> bool {
183176
///
184177
/// Gets an attribute of the given value as the given type.
185178
///
186-
fn getattr<T>(value: &Value, field: &str) -> Result<T, String>
179+
fn getattr<T>(value: &PyObject, field: &str) -> Result<T, String>
187180
where
188181
for<'a> T: FromPyObject<'a>,
189182
{
@@ -235,27 +228,27 @@ fn collect_iterable(value: &Value) -> Result<Vec<Value>, String> {
235228
/// Pulls out the value specified by the field name from a given Value
236229
///
237230
pub fn project_ignoring_type(value: &Value, field: &str) -> Value {
238-
getattr(value, field).unwrap()
231+
getattr(value.as_ref(), field).unwrap()
239232
}
240233

241234
pub fn project_iterable(value: &Value) -> Vec<Value> {
242235
collect_iterable(value).unwrap()
243236
}
244237

245238
pub fn project_multi(value: &Value, field: &str) -> Vec<Value> {
246-
getattr(value, field).unwrap()
239+
getattr(value.as_ref(), field).unwrap()
247240
}
248241

249242
pub fn project_bool(value: &Value, field: &str) -> bool {
250-
getattr(value, field).unwrap()
243+
getattr(value.as_ref(), field).unwrap()
251244
}
252245

253246
pub fn project_multi_strs(value: &Value, field: &str) -> Vec<String> {
254-
getattr(value, field).unwrap()
247+
getattr(value.as_ref(), field).unwrap()
255248
}
256249

257250
pub fn project_frozendict(value: &Value, field: &str) -> BTreeMap<String, String> {
258-
let frozendict = Value::new(getattr(value, field).unwrap());
251+
let frozendict = getattr(value.as_ref(), field).unwrap();
259252
let pydict: PyDict = getattr(&frozendict, "_data").unwrap();
260253
let gil = Python::acquire_gil();
261254
let py = gil.python();
@@ -271,25 +264,25 @@ pub fn project_str(value: &Value, field: &str) -> String {
271264
// cloning in some cases.
272265
// TODO: We can't directly extract as a string here, because val_to_str defaults to empty string
273266
// for None.
274-
val_to_str(&getattr(value, field).unwrap())
267+
val_to_str(&getattr(value.as_ref(), field).unwrap())
275268
}
276269

277270
pub fn project_u64(value: &Value, field: &str) -> u64 {
278-
getattr(value, field).unwrap()
271+
getattr(value.as_ref(), field).unwrap()
279272
}
280273

281274
pub fn project_maybe_u64(value: &Value, field: &str) -> Result<u64, String> {
282-
getattr(value, field)
275+
getattr(value.as_ref(), field)
283276
}
284277

285278
pub fn project_f64(value: &Value, field: &str) -> f64 {
286-
getattr(value, field).unwrap()
279+
getattr(value.as_ref(), field).unwrap()
287280
}
288281

289282
pub fn project_bytes(value: &Value, field: &str) -> Vec<u8> {
290283
// TODO: It's possible to view a python bytes as a `&[u8]`, so we could avoid actually
291284
// cloning in some cases.
292-
getattr(value, field).unwrap()
285+
getattr(value.as_ref(), field).unwrap()
293286
}
294287

295288
pub fn key_to_str(key: &Key) -> String {
@@ -327,41 +320,31 @@ pub fn create_exception(msg: &str) -> Value {
327320
Value::from(with_externs(|py, e| e.call_method(py, "create_exception", (msg,), None)).unwrap())
328321
}
329322

330-
pub fn check_for_python_none(value: Value) -> Option<Value> {
323+
pub fn check_for_python_none(value: PyObject) -> Option<PyObject> {
331324
let gil = Python::acquire_gil();
332325
let py = gil.python();
333326

334-
if *value == py.None() {
327+
if value == py.None() {
335328
return None;
336329
}
337330
Some(value)
338331
}
339332

340-
pub fn call_method(value: &Value, method: &str, args: &[Value]) -> Result<Value, Failure> {
333+
pub fn call_method(value: &PyObject, method: &str, args: &[Value]) -> Result<PyObject, PyErr> {
341334
let arg_handles: Vec<PyObject> = args.iter().map(|v| v.clone().into()).collect();
342335
let gil = Python::acquire_gil();
343336
let args_tuple = PyTuple::new(gil.python(), &arg_handles);
344-
value
345-
.call_method(gil.python(), method, args_tuple, None)
346-
.map(Value::from)
347-
.map_err(|py_err| Failure::from_py_err(gil.python(), py_err))
337+
value.call_method(gil.python(), method, args_tuple, None)
348338
}
349339

350-
pub fn call_function(func: &Value, args: &[Value]) -> Result<PyObject, PyErr> {
340+
pub fn call_function<T: AsRef<PyObject>>(func: T, args: &[Value]) -> Result<PyObject, PyErr> {
341+
let func: &PyObject = func.as_ref();
351342
let arg_handles: Vec<PyObject> = args.iter().map(|v| v.clone().into()).collect();
352343
let gil = Python::acquire_gil();
353344
let args_tuple = PyTuple::new(gil.python(), &arg_handles);
354345
func.call(gil.python(), args_tuple, None)
355346
}
356347

357-
pub fn call(func: &Value, args: &[Value]) -> Result<Value, Failure> {
358-
let output = call_function(func, args);
359-
output.map(Value::from).map_err(|py_err| {
360-
let gil = Python::acquire_gil();
361-
Failure::from_py_err(gil.python(), py_err)
362-
})
363-
}
364-
365348
pub fn generator_send(generator: &Value, arg: &Value) -> Result<GeneratorResponse, Failure> {
366349
let response = with_externs(|py, e| {
367350
e.call_method(
@@ -370,7 +353,7 @@ pub fn generator_send(generator: &Value, arg: &Value) -> Result<GeneratorRespons
370353
(generator as &PyObject, arg as &PyObject),
371354
None,
372355
)
373-
.map_err(|py_err| Failure::from_py_err(py, py_err))
356+
.map_err(|py_err| Failure::from_py_err_with_gil(py, py_err))
374357
})?;
375358

376359
let gil = Python::acquire_gil();
@@ -398,7 +381,7 @@ pub fn generator_send(generator: &Value, arg: &Value) -> Result<GeneratorRespons
398381
.map(|g| {
399382
let get = g
400383
.cast_as::<PyGeneratorResponseGet>(py)
401-
.map_err(|e| Failure::from_py_err(py, e.into()))?;
384+
.map_err(|e| Failure::from_py_err_with_gil(py, e.into()))?;
402385
Ok(Get::new(py, interns, get)?)
403386
})
404387
.collect::<Result<Vec<_>, _>>()?;
@@ -520,7 +503,7 @@ impl Get {
520503
output: interns.type_insert(py, get.product(py).clone_ref(py)),
521504
input: interns
522505
.key_insert(py, get.subject(py).clone_ref(py).into())
523-
.map_err(|e| Failure::from_py_err(py, e))?,
506+
.map_err(|e| Failure::from_py_err_with_gil(py, e))?,
524507
input_type: Some(interns.type_insert(py, get.declared_subject(py).clone_ref(py))),
525508
})
526509
}

src/rust/engine/src/intrinsics.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::nodes::{
77
};
88
use crate::tasks::Intrinsic;
99
use crate::types::Types;
10+
use crate::Failure;
1011

1112
use fs::RelativePath;
1213
use futures::compat::Future01CompatExt;
@@ -304,7 +305,7 @@ fn download_file_to_digest(
304305
mut args: Vec<Value>,
305306
) -> BoxFuture<'static, NodeResult<Value>> {
306307
async move {
307-
let key = externs::acquire_key_for(args.pop().unwrap())?;
308+
let key = externs::key_for(args.pop().unwrap()).map_err(Failure::from_py_err)?;
308309
let digest = context.get(DownloadedFile(key)).await?;
309310
Snapshot::store_directory_digest(&digest).map_err(|s| throw(&s))
310311
}
@@ -354,7 +355,7 @@ fn create_digest_to_digest(
354355
let path = RelativePath::new(PathBuf::from(path))
355356
.map_err(|e| format!("The `path` must be relative: {:?}", e))?;
356357

357-
if externs::hasattr(&file_content_or_directory, "content") {
358+
if externs::hasattr(file_content_or_directory.as_ref(), "content") {
358359
let bytes = bytes::Bytes::from(externs::project_bytes(
359360
&file_content_or_directory,
360361
"content",

src/rust/engine/src/nodes.rs

+5-3
Original file line numberDiff line numberDiff line change
@@ -910,13 +910,13 @@ impl Task {
910910
.cloned()
911911
.ok_or_else(|| match get.input_type {
912912
Some(ty) if externs::is_union(ty) => {
913-
let value = externs::type_for_type_id(ty).into_object().into();
913+
let value = externs::type_for_type_id(ty).into_object();
914914
match externs::call_method(
915915
&value,
916916
"non_member_error_message",
917917
&[externs::val_for(&get.input)],
918918
) {
919-
Ok(err_msg) => throw(&externs::val_to_str(&err_msg)),
919+
Ok(err_msg) => throw(&externs::val_to_str(&err_msg.into())),
920920
// If the non_member_error_message() call failed for any reason,
921921
// fall back to a generic message.
922922
Err(_e) => throw(&format!(
@@ -1025,7 +1025,9 @@ impl WrappedNode for Task {
10251025
let product = self.product;
10261026
let can_modify_workunit = self.task.can_modify_workunit;
10271027

1028-
let mut result_val = externs::call(&externs::val_for(&func.0), &deps)?;
1028+
let result_val =
1029+
externs::call_function(&externs::val_for(&func.0), &deps).map_err(Failure::from_py_err)?;
1030+
let mut result_val: Value = result_val.into();
10291031
let mut result_type = externs::get_type_for(&result_val);
10301032
if result_type == context.core.types.coroutine {
10311033
result_val = Self::generate(context.clone(), params, entry, result_val).await?;

src/rust/engine/src/scheduler.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -591,7 +591,7 @@ fn maybe_break_execution_loop(python_signal_fn: &Value) -> Option<ExecutionTermi
591591
{
592592
Some(ExecutionTermination::KeyboardInterrupt)
593593
} else {
594-
let failure = Failure::from_py_err(py, e);
594+
let failure = Failure::from_py_err_with_gil(py, e);
595595
std::mem::drop(gil);
596596
Some(ExecutionTermination::Fatal(format!(
597597
"Error when checking Python signal state: {}",

0 commit comments

Comments
 (0)