Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generated extension functions are weakly typed #108

Closed
Vlad-Shcherbina opened this issue Feb 9, 2018 · 8 comments
Closed

Generated extension functions are weakly typed #108

Vlad-Shcherbina opened this issue Feb 9, 2018 · 8 comments

Comments

@Vlad-Shcherbina
Copy link
Contributor

Consider the README example:

[pyfn(m, "sum_as_string")]
fn sum_as_string_py(a:i64, b:i64) -> PyResult<String>

Here is what happens when this function is called with non-int arguments:

>>> from rust2py import sum_as_string
>>> sum_as_string(1, 2)  # good
'3'

>>> sum_as_string(10**100, 2)  # good
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OverflowError: int too big to convert

>>> sum_as_string([], [])  # good
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: int() argument must be a string, a bytes-like object or a number, not 'list'

>>> sum_as_string('1', 2)  # bad
'3'

>>> sum_as_string(1.9, 2.9)  # bad
'3'

Generally Python functions that expect int argument raise TypeError when fed strings or floats. This helps to catch some programming errors.

>>> range('42')  # good
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object cannot be interpreted as an integer

>>> range(42.9)  # good
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'float' object cannot be interpreted as an integer

I think it would be more consistent if generated extension functions behaved the same way.

@fafhrd91
Copy link
Contributor

fafhrd91 commented Feb 9, 2018

thanks! this is definitely a bug.

@Vlad-Shcherbina
Copy link
Contributor Author

So it seems relevant that I'm running this example on Windows (target_pointer_width="64", target_os="windows"), and that the argument type is i64 and not, say, i32.

The problem appears to be here:

pyo3/src/objects/num3.rs

Lines 121 to 130 in 63a2066

if ffi::PyLong_Check(ptr) != 0 {
err_if_invalid_value(ob.py(), !0, $pylong_as_ull_or_ull(ptr))
} else {
let num = ffi::PyNumber_Long(ptr);
if num.is_null() {
Err(PyErr::fetch(ob.py()))
} else {
err_if_invalid_value(ob.py(), !0, $pylong_as_ull_or_ull(num))
}
}

PyLong_Check(x) is roughly equivalent to isinstance(x, int), and PyNumber_Long(x) is analagous to int(x), hence the frivolous conversion.

This code was there since it was written in dgrunwald/rust-cpython@55ea6cc#diff-2eab662fc2799e2bdab71b4844e11090R155

I wonder if this whole else branch should just return TypeError instead.

@fafhrd91
Copy link
Contributor

but ffi::PyLong_Check(ptr) should fail for string, I guess

@Vlad-Shcherbina
Copy link
Contributor Author

PyLong_Check() returns 0 on a string, and then PyNumber_Long() attempts to parse it.

@fafhrd91
Copy link
Contributor

Unexpected :)

@Vlad-Shcherbina
Copy link
Contributor Author

Found another similar instance of weak-typedness that happens for a different reason:

#[pyfn(m, "take_i32")]
fn take_i32(a: i32) -> PyResult<()> {
    println!("my arg is {}", a);
    Ok(())
}
>>> take_i32(42)  # good
my arg is 42

>>> take_i32('42')  # good
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: an integer is required (got type str)

>>> take_i32(42.9)  # bad
my arg is 42

This happens because in

pyo3/src/objects/num3.rs

Lines 48 to 57 in 63a2066

pyobject_extract!(obj to $rust_type => {
let val = unsafe { ffi::PyLong_AsLong(obj.as_ptr()) };
if val == -1 && PyErr::occurred(obj.py()) {
return Err(PyErr::fetch(obj.py()));
}
match cast::<c_long, $rust_type>(val) {
Some(v) => Ok(v),
None => Err(exc::OverflowError.into())
}
});

, the call to PyLong_AsLong() is equivalent to calling __int__() method on an object. Strings don't have it but floats do.

Vlad-Shcherbina added a commit to Vlad-Shcherbina/pyo3 that referenced this issue Feb 11, 2018
Neither Python nor Rust has implicit float to integer conversions
(they mask programming errors), so it would make sense for the
bindings library to disallow them as well.

Later there will be tests that such conversions result in TypeError.
Vlad-Shcherbina added a commit to Vlad-Shcherbina/pyo3 that referenced this issue Feb 11, 2018
In `int_fits_c_long!`,
use `PyLong_AsLong(PyNumber_Index(x))`
instead of `PyNumber_Index(x)`.

In `int_convert_u64_or_i64!`,
use `PyLong_As*LongLong(PyNumber_Index(x))`
instead of `PyLong_As*LongLong(if PyLong_Check(x) {x} else {PyNumber_Long(x)})`.

Along the way, fix memory leak caused by missing `Py_DECREF(num)`.


`PyNumber_Index(x)` is the best way to get an integer losslessly:
https://docs.python.org/3/reference/datamodel.html#object.__index__
https://docs.python.org/3.5/c-api/number.html#c.PyNumber_Index

`PyLong_AsLong(x)` has the problem that it attempts to call `x.__int__()`.
Strings don't implement this method, but floats do, so it silently converts
floats to integers.
https://docs.python.org/3.5/c-api/long.html#c.PyLong_AsLong

`PyNumber_Long(x)` is equivalent to `int(x)` call,
so not only does it truncate floats, but also attempts to parse strings.
https://docs.python.org/3.5/c-api/number.html#c.PyNumber_Long

`PyLong_Check(x)` is redundant because it happens inside `PyNumber_Index(x)`
anyway:
https://github.com/python/cpython/blob/988fb28431d3a3fc5dc6eb657c8a4baacc04d9ce/Objects/abstract.c#L1259
fafhrd91 added a commit that referenced this issue Feb 11, 2018
Disallow str/float to int conversions #108
@fafhrd91
Copy link
Contributor

@Vlad-Shcherbina i think this should be fixed now?

@Vlad-Shcherbina
Copy link
Contributor Author

Yes, it's fixed for Python 3.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants