blob: 16390ed310b6c3a1b7ecd46c87e6e2bdfae93987 [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# error handling for FFI
import types
import re
from typing import Callable, Optional
ERROR_NAME_TO_TYPE: dict[str, type] = {}
ERROR_TYPE_TO_NAME: dict[type, str] = {}
_WITH_APPEND_BACKTRACE: Optional[Callable[[BaseException, str], BaseException]] = None
_TRACEBACK_TO_BACKTRACE_STR: Optional[Callable[[types.TracebackType | None], str]] = None
cdef class Error(CObject):
"""Base class for FFI errors.
An :class:`Error` is a lightweight wrapper around a concrete Python
exception raised by FFI calls. It stores the error ``kind`` (e.g.
``"ValueError"``), the message, and a serialized FFI backtrace that
can be re-attached to produce a Python traceback.
Users normally interact with specific error subclasses that are
registered via :func:`tvm_ffi.error.register_error`.
Notes
-----
Do not directly raise this object. Instead, use :py:meth:`py_error`
to convert it to a Python exception and raise that.
"""
__slots__ = ()
def __init__(self, kind: str, message: str, backtrace: str):
"""Construct an error wrapper.
Parameters
----------
kind : str
Name of the Python exception type (e.g. ``"ValueError"``).
message : str
The error message from the FFI side.
backtrace : str
Serialized backtrace encoded by the runtime.
"""
cdef ByteArrayArg kind_arg = ByteArrayArg(c_str(kind))
cdef ByteArrayArg message_arg = ByteArrayArg(c_str(message))
cdef ByteArrayArg backtrace_arg = ByteArrayArg(c_str(backtrace))
cdef TVMFFIObjectHandle out
cdef int ret = TVMFFIErrorCreate(
kind_arg.cptr(), message_arg.cptr(), backtrace_arg.cptr(), &out
)
if ret != 0:
raise MemoryError("Failed to create error object")
(<CObject>self).chandle = out
def update_backtrace(self, backtrace: str) -> None:
"""Replace the stored backtrace string with ``backtrace``.
Parameters
----------
backtrace : str
The backtrace to store. The internal storage is reverse of
Python's traceback order to simplify appending during
propagation; it is reversed again when rendered.
"""
cdef ByteArrayArg backtrace_arg = ByteArrayArg(c_str(backtrace))
TVMFFIErrorGetCellPtr(self.chandle).update_backtrace(
self.chandle, backtrace_arg.cptr(), kTVMFFIBacktraceUpdateModeReplace
)
def py_error(self) -> BaseException:
"""Return a Python :class:`BaseException` instance for this error."""
error_cls = ERROR_NAME_TO_TYPE.get(self.kind, RuntimeError)
py_error = error_cls(self.message)
py_error = _WITH_APPEND_BACKTRACE(py_error, self.backtrace)
py_error.__tvm_ffi_error__ = self
return py_error
@property
def kind(self):
return bytearray_to_str(&(TVMFFIErrorGetCellPtr(self.chandle).kind))
@property
def message(self):
return bytearray_to_str(&(TVMFFIErrorGetCellPtr(self.chandle).message))
@property
def backtrace(self):
return bytearray_to_str(&(TVMFFIErrorGetCellPtr(self.chandle).backtrace))
@property
def extra_context(self):
"""Optional structured payload attached to this error.
Returns ``None`` if nothing is attached. May be inspected via the
appropriate type-specific helpers.
"""
cdef TVMFFIObjectHandle ctx_handle = TVMFFIErrorGetCellPtr(self.chandle).extra_context
if ctx_handle == NULL:
return None
# Build an owned Any from the unowned handle by incrementing the refcount.
cdef TVMFFIAny any_val
any_val.type_index = TVMFFIObjectGetTypeIndex(ctx_handle)
any_val.v_obj = <TVMFFIObject*>ctx_handle
TVMFFIObjectIncRef(ctx_handle)
return make_ret_object(any_val)
cdef inline Error move_from_last_error():
# raise last error
error = Error.__new__(Error)
TVMFFIErrorMoveFromRaised(&(<CObject>error).chandle)
return error
cdef inline int raise_existing_error() except -2:
return -2
cdef inline int set_last_ffi_error(error) except -1:
"""Set the last FFI error"""
cdef Error ffi_error
kind = ERROR_TYPE_TO_NAME.get(type(error), "RuntimeError")
message = error.__str__()
# NOTE: backtrace storage convention is reverse of python traceback
py_backtrace = _TRACEBACK_TO_BACKTRACE_STR(error.__traceback__)
c_backtrace = bytearray_to_str(TVMFFIBacktrace(NULL, 0, NULL, 0))
# error comes from an exception thrown from C++ side
if hasattr(error, "__tvm_ffi_error__"):
# already have stack trace
ffi_error = error.__tvm_ffi_error__
# attach the python backtrace together with the C++ backtrace to get full trace
ffi_error.update_backtrace(py_backtrace + c_backtrace)
TVMFFIErrorSetRaised(ffi_error.chandle)
else:
ffi_error = Error(kind, message, py_backtrace + c_backtrace)
TVMFFIErrorSetRaised(ffi_error.chandle)
def _convert_to_ffi_error(error: BaseException) -> Error:
"""Convert the python error to the FFI error"""
py_backtrace = _TRACEBACK_TO_BACKTRACE_STR(error.__traceback__)
if hasattr(error, "__tvm_ffi_error__"):
error.__tvm_ffi_error__.update_backtrace(py_backtrace)
return error.__tvm_ffi_error__
else:
kind = ERROR_TYPE_TO_NAME.get(type(error), "RuntimeError")
message = error.__str__()
return Error(kind, message, py_backtrace)
cdef public int TVMFFICyErrorSetRaisedFromPyError(PyObject* py_err) noexcept:
"""Set the last FFI error from a Python exception.
Parameters
----------
py_err : PyObject*
The Python exception to set as the last FFI error.
"""
set_last_ffi_error(<object>py_err)
return -1
cdef inline int CHECK_CALL(int ret) except -2:
"""Check the return code of the C API function call"""
if ret == 0:
return 0
# backward compact with error already set case
# TODO(tqchen): remove after we move beyond a few versions.
if ret == -2:
raise raise_existing_error()
error = move_from_last_error()
if error.kind == "EnvErrorAlreadySet":
raise raise_existing_error()
raise error.py_error()