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

Python error handling #215

Merged
merged 14 commits into from
Aug 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/arithmetic/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ name = "uniffi_arithmetic"
[dependencies]
uniffi_macros = {path = "../../uniffi_macros"}
uniffi = {path = "../../uniffi", features=["builtin-bindgen"]}
thiserror = "1.0"

[build-dependencies]
uniffi_build = {path = "../../uniffi_build", features=["builtin-bindgen"]}
15 changes: 9 additions & 6 deletions examples/arithmetic/src/arithmetic.idl
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@

enum Overflow {
"WRAPPING",
"SATURATING",
[Error]
enum ArithmeticError {
"IntegerOverflow",
};

namespace arithmetic {
u64 add(u64 a, u64 b, optional Overflow overflow = "WRAPPING");
u64 sub(u64 a, u64 b, optional Overflow overflow = "WRAPPING");
[Throws=ArithmeticError]
u64 add(u64 a, u64 b);

[Throws=ArithmeticError]
u64 sub(u64 a, u64 b);

boolean equal(u64 a, u64 b);
};
27 changes: 11 additions & 16 deletions examples/arithmetic/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,24 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// Flag to let calling code specify a particular behaviour of integer overflow,
// since rust has such nice support for these.
pub enum Overflow {
WRAPPING,
SATURATING,
#[derive(Debug, thiserror::Error)]
enum ArithmeticError {
#[error("Integer overflow!")]
IntegerOverflow,
}

pub fn add(a: u64, b: u64, overflow: Overflow) -> u64 {
match overflow {
Overflow::WRAPPING => a.overflowing_add(b).0,
Overflow::SATURATING => a.saturating_add(b),
}
fn add(a: u64, b: u64) -> Result<u64> {
a.checked_add(b).ok_or(ArithmeticError::IntegerOverflow)
}

pub fn sub(a: u64, b: u64, overflow: Overflow) -> u64 {
match overflow {
Overflow::WRAPPING => a.overflowing_sub(b).0,
Overflow::SATURATING => a.saturating_sub(b),
}
fn sub(a: u64, b: u64) -> Result<u64> {
a.checked_sub(b).ok_or(ArithmeticError::IntegerOverflow)
}

pub fn equal(a: u64, b: u64) -> bool {
fn equal(a: u64, b: u64) -> bool {
a == b
}

type Result<T, E = ArithmeticError> = std::result::Result<T, E>;

include!(concat!(env!("OUT_DIR"), "/arithmetic.uniffi.rs"));
18 changes: 16 additions & 2 deletions examples/arithmetic/tests/bindings/test_arithmetic.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
import uniffi.arithmetic.*;

assert(add(2, 3, Overflow.SATURATING) == 5L)
assert(add(2, 4) == 6L)
assert(add(4, 8) == 12L)

try {
sub(0, 2)
throw RuntimeException("Should have thrown a IntegerOverflow exception!")
} catch (e: ArithmeticErrorException) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In swift and python we explicitly test that this was the IntegerOverflow variant, but here we test only that it was an ArithmeticError. Does it work if we catch ArithmeticErrorException.IntegerOverflow here like the other languages do?

(I note that the todolist example has a similar problem, but we don't need to fix that here)

// It's okay!
}

assert(sub(4, 2) == 2L)
assert(sub(8, 4) == 4L)

assert(equal(2, 2))
assert(equal(4, 4))
assert(!equal(4, 5))

assert(!equal(2, 4))
assert(!equal(4, 8))
25 changes: 23 additions & 2 deletions examples/arithmetic/tests/bindings/test_arithmetic.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
from arithmetic import *

assert add(2, 3, Overflow.SATURATING) == 5
try:
add(18446744073709551615, 1)
assert(not("Should have thrown a IntegerOverflow exception!"))
except ArithmeticError.IntegerOverflow:
# It's okay!
pass

assert add(2, 4) == 6
assert add(4, 8) == 12

try:
sub(0, 1)
assert(not("Should have thrown a IntegerOverflow exception!"))
except ArithmeticError.IntegerOverflow:
# It's okay!
pass

assert sub(4, 2) == 2
assert sub(8, 4) == 4

assert equal(2, 2)
assert equal(4, 4)
assert not equal(4,5)

assert not equal(2, 4)
assert not equal(4, 8)
27 changes: 24 additions & 3 deletions examples/arithmetic/tests/bindings/test_arithmetic.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
import arithmetic

assert(add(a: 2, b: 3, overflow: .saturating) == 5, "addition works")
do {
let _ = try add(a: 18446744073709551615, b: 1)
fatalError("Should have thrown a IntegerOverflow exception!")
} catch ArithmeticError.IntegerOverflow {
// It's okay!
}

assert(equal(a: 4, b: 4), "equality works")
assert(!equal(a: 4, b: 5), "non-equality works")
assert(try! add(a: 2, b: 4) == 6, "add work")
assert(try! add(a: 4, b: 8) == 12, "add work")

do {
let _ = try sub(a: 0, b: 1)
fatalError("Should have thrown a IntegerOverflow exception!")
} catch ArithmeticError.IntegerOverflow {
// It's okay!
}

assert(try! sub(a: 4, b: 2) == 2, "sub work")
assert(try! sub(a: 8, b: 4) == 4, "sub work")

assert(equal(a: 2, b: 2), "equal works")
assert(equal(a: 4, b: 4), "equal works")

assert(!equal(a: 2, b: 4), "non-equal works")
assert(!equal(a: 4, b: 8), "non-equal works")
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ internal open class {{e.name()}} : RustError() {
{% for value in e.values() -%}
{{loop.index}} -> return {{e.name()}}Exception.{{value}}(message) as E
{% endfor -%}
else -> throw RuntimeException("Invalid error recieved...")
else -> throw RuntimeException("Invalid error received...")
weslenng marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Expand Down
44 changes: 44 additions & 0 deletions uniffi_bindgen/src/bindings/python/templates/ErrorTemplate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
class RustError(ctypes.Structure):
_fields_ = [
("code", ctypes.c_int32),
("message", ctypes.c_void_p),
]

def free(self):
return _UniFFILib.{{ ci.ffi_string_free().name() }}(self.message)

def __str__(self):
return "RustError(code={}, message={})".format(
self.code,
str(ctypes.cast(self.message, ctypes.c_char_p).value, "utf-8"),
)

{% for e in ci.iter_error_definitions() %}
class {{ e.name()|class_name_py }}:
{%- for value in e.values() %}
class {{ value|class_name_py }}(Exception):
pass
{%- endfor %}
weslenng marked this conversation as resolved.
Show resolved Hide resolved

@staticmethod
def raise_err(code, message):
{%- for value in e.values() %}
if code == {{ loop.index }}:
raise {{ e.name()|class_name_py }}.{{ value|class_name_py }}(message)
{% endfor %}
raise Exception("Unknown error code")
{% endfor %}

def rust_call_with_error(error_class, fn, *args):
error = RustError()
weslenng marked this conversation as resolved.
Show resolved Hide resolved
error.code = 0

args_with_error = args + (ctypes.byref(error),)
result = fn(*args_with_error)
if error.code != 0:
message = str(error)
error.free()

error_class.raise_err(error.code, message)

return result
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def loadIndirect(componentName):
_UniFFILib = loadIndirect(componentName="{{ ci.namespace() }}")
{%- for func in ci.iter_ffi_function_definitions() %}
_UniFFILib.{{ func.name() }}.argtypes = (
{%- call py::arg_list_ffi_decl(func.arguments()) -%}
{%- call py::arg_list_ffi_decl(func) -%}
)
_UniFFILib.{{ func.name() }}.restype = {% match func.return_type() %}{% when Some with (type_) %}{{ type_|type_ffi }}{% when None %}None{% endmatch %}
{%- endfor %}
12 changes: 6 additions & 6 deletions uniffi_bindgen/src/bindings/python/templates/ObjectTemplate.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
class {{ obj.name()|class_name_py }}(object):
# XXX TODO: support for multiple constructors...
{%- for cons in obj.constructors() %}
def __init__(self, {% call py::arg_list_decl(cons.arguments()) -%}):
{%- call py::coerce_args_extra_indent(cons.arguments()) %}
def __init__(self, {% call py::arg_list_decl(cons) -%}):
{%- call py::coerce_args_extra_indent(cons) %}
self._handle = {% call py::to_ffi_call(cons) %}
{%- endfor %}

Expand All @@ -13,14 +13,14 @@ def __del__(self):
{%- match meth.return_type() -%}

{%- when Some with (return_type) -%}
def {{ meth.name()|fn_name_py }}(self, {% call py::arg_list_decl(meth.arguments()) %}):
{%- call py::coerce_args_extra_indent(meth.arguments()) %}
def {{ meth.name()|fn_name_py }}(self, {% call py::arg_list_decl(meth) %}):
{%- call py::coerce_args_extra_indent(meth) %}
_retval = {% call py::to_ffi_call_with_prefix("self._handle", meth) %}
return {{ "_retval"|lift_py(return_type) }}

{%- when None -%}
def {{ meth.name()|fn_name_py }}(self, {% call py::arg_list_decl(meth.arguments()) %}):
{%- call py::coerce_args_extra_indent(meth.arguments()) %}
def {{ meth.name()|fn_name_py }}(self, {% call py::arg_list_decl(meth) %}):
{%- call py::coerce_args_extra_indent(meth) %}
{% call py::to_ffi_call_with_prefix("self._handle", meth) %}
{% endmatch %}
{% endfor %}
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{%- match func.return_type() -%}
{%- when Some with (return_type) %}

def {{ func.name()|fn_name_py }}({%- call py::arg_list_decl(func.arguments()) -%}):
{%- call py::coerce_args(func.arguments()) %}
def {{ func.name()|fn_name_py }}({%- call py::arg_list_decl(func) -%}):
{%- call py::coerce_args(func) %}
_retval = {% call py::to_ffi_call(func) %}
return {{ "_retval"|lift_py(return_type) }}

{% when None -%}

def {{ func.name()|fn_name_py }}({%- call py::arg_list_decl(func.arguments()) -%}):
{%- call py::coerce_args(func.arguments()) %}
def {{ func.name()|fn_name_py }}({%- call py::arg_list_decl(func) -%}):
{%- call py::coerce_args(func) %}
{% call py::to_ffi_call(func) %}
{% endmatch %}
43 changes: 26 additions & 17 deletions uniffi_bindgen/src/bindings/python/templates/macros.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,27 @@
#}

{%- macro to_ffi_call(func) -%}
_UniFFILib.{{ func.ffi_func().name() }}({% call _arg_list_ffi_call(func.arguments()) -%})
{%- match func.throws() -%}
{%- when Some with (e) -%}
rust_call_with_error({{ e|class_name_py }},_UniFFILib.{{ func.ffi_func().name() }}{% if func.arguments().len() > 0 %},{% endif %}{% call _arg_list_ffi_call(func) -%})
{%- else -%}
_UniFFILib.{{ func.ffi_func().name() }}({% call _arg_list_ffi_call(func) -%})
{%- endmatch -%}
{%- endmacro -%}

{%- macro to_ffi_call_with_prefix(prefix, func) -%}
_UniFFILib.{{ func.ffi_func().name() }}(
{{- prefix }}{% if func.arguments().len() > 0 %}, {% call _arg_list_ffi_call(func.arguments()) -%}{% endif -%}
)
{%- match func.throws() -%}
{%- when Some with (e) -%}
rust_call_with_error({{ e|class_name_py }},_UniFFILib.{{ func.ffi_func().name() }},{{- prefix }}{% if func.arguments().len() > 0 %},{% endif %}{% call _arg_list_ffi_call(func) %}))
{%- else -%}
_UniFFILib.{{ func.ffi_func().name() }}({{- prefix }}{% if func.arguments().len() > 0 %},{% endif %}{% call _arg_list_ffi_call(func) %}))
{%- endmatch -%}
{%- endmacro -%}

{%- macro _arg_list_ffi_call(args) %}
{%- for arg in args %}
{%- macro _arg_list_ffi_call(func) %}
{%- for arg in func.arguments() %}
{{- arg.name()|lower_py(arg.type_()) }}
{%- if !loop.last %}, {% endif %}
{%- if !loop.last %},{% endif %}
{%- endfor %}
{%- endmacro -%}

Expand All @@ -26,31 +34,32 @@
// Note the var_name_py and type_py filters.
-#}

{% macro arg_list_decl(args) %}
{%- for arg in args -%}
{% macro arg_list_decl(func) %}
{%- for arg in func.arguments() -%}
{{ arg.name()|var_name_py }}
{%- if !loop.last %}, {% endif -%}
{%- if !loop.last %},{% endif -%}
{%- endfor %}
{%- endmacro %}

{#-
// Arglist as used in the _UniFFILib function declations.
// Note unfiltered name but type_ffi filters.
-#}
{%- macro arg_list_ffi_decl(args) %}
{%- for arg in args -%}
{{ arg.type_()|type_ffi }}, {##}
{%- macro arg_list_ffi_decl(func) %}
{%- for arg in func.arguments() -%}
{{ arg.type_()|type_ffi }},{##}
{%- endfor %}
{%- if func.has_out_err() -%}ctypes.POINTER(RustError),{%- endif -%}
{%- endmacro -%}

{%- macro coerce_args(args) %}
{%- for arg in args %}
{%- macro coerce_args(func) %}
{%- for arg in func.arguments() %}
{{ arg.name()|coerce_py(arg.type_()) -}}
{% endfor -%}
{%- endmacro -%}

{%- macro coerce_args_extra_indent(args) %}
{%- for arg in args %}
{%- macro coerce_args_extra_indent(func) %}
{%- for arg in func.arguments() %}
{{ arg.name()|coerce_py(arg.type_()) }}
{%- endfor %}
{%- endmacro -%}
8 changes: 7 additions & 1 deletion uniffi_bindgen/src/bindings/python/templates/wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@

{% include "RustBufferHelper.py" %}

# Error definitions
{% include "ErrorTemplate.py" %}

{% include "NamespaceLibraryTemplate.py" %}

# Public interface members begin here.
Expand Down Expand Up @@ -56,6 +59,9 @@
{%- for obj in ci.iter_object_definitions() %}
"{{ obj.name()|class_name_py }}",
{%- endfor %}
{%- for e in ci.iter_error_definitions() %}
"{{ e.name()|class_name_py }}",
{%- endfor %}
]

{% import "macros.py" as py %}
{% import "macros.py" as py %}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{#
// In here we define conversions between a native reference to Swift errors
// We use the RustError protocol to define the requirments. Any implementers of the protocol
// We use the RustError protocol to define the requirements. Any implementers of the protocol
// Can be generated from a NativeRustError.

#}
Expand Down