mathworks / libmexclass

libmexclass is a MATLAB framework which enables users to implement the functionality of MATLAB classes in terms of equivalent C++ classes using MEX.
BSD 3-Clause "New" or "Revised" License
5 stars 1 forks source link

Consider having the ProxyManager return "Result"-like or "Status"-like objects for error handling #33

Closed sgilmore10 closed 1 year ago

sgilmore10 commented 1 year ago

NEW DESCRIPTION

Summary

It would be nice if libmexclass provided a mechanism for clients to throw MATLAB-facing errors without forcing clients to call feval via the MATLABEngine themselves.

Required Changes

  1. Add a struct called libmexclass::error::Error that has two fields: id and message.

  2. Add a field named error to libmexclass::proxy::method::Context, which is passed to all proxy method classes. This field should be a std::optional<libmexclass::error::Error>. By default, error will be a nullopt. If clients want libmexclass to throw a MATLAB-facing error, they should set this field to a non-nullopt value in their proxy class methods. Here's an example of this workflow in example/proxy/Car.cpp:

void Car::SetSpeed(libmexclass::proxy::method::Context& context) {
    matlab::data::TypedArray<uint64_t> speed_mda = context.inputs[0];
    const std::uint64_t speed = uint64_t(speed_mda[0]);
    if (speed > 100) {
        // force libmexclass to throw an error if the desired speed is greate than 100 
        context.error = libmexclass::error::Error{"Car:Speed:TooFast", "Slow down!!"};
        return;
    }
    car.SetSpeed(speed);
}
  1. Change the return value of libmexclass::action::Action::execute() from void to std::optional<libmexclass::error::Error. Update the execute() of the concrete subclasses of Action (i.e. Create, Delete, MethodCall) to return the error field stored on the context.

  2. In the gateway function, capture the return value of action->execute(). If return value is not a nullopt, throw a MATLAB-facing error using feval. The error id and message should be taken from the id and message fields of the libmexclass::error::Error struct returned by execute(). Here's what this will look like:

std::optional<libmexclass::error::Errror> maybe_error = action->execute();
if (maybe_error) {
    matlab::data::ArrayFactory factory;
    libmexclass::error::Errror error = maybe_error.value();

    // Construct the error struct
     matlab::data::StructArray errorStruct = factory.createStructArray({1, 1}, {"identifier", "message"});
     errorStruct[0]["identifier"] = factory.createScalar(error.id);
     errorStruct[0]["message"] = factory.createScalar(error.message);

   // throw the error in MATLAB  
    matlab->feval(u"error", 0, std::vector<matlab::data::Array>({errorStruct}));
}
  1. To support throwing exceptions at construction time, change the REGISTER_PROXY macro to the following:
#define REGISTER_PROXY(name, cppClass) if (class_name.compare(#name) == 0) return cppClass::make(constructor_arguments)

This requires all proxy classes to define a a static make function:

static limexclass::proxy::MakeResult make(const libmexclass::proxy::FunctionArguments& constructor_arguments) 

libmexclass::proxy::MakeResult is a typedef to std::variant<std::shared_ptr<libmexclass::proxy::Proxy>, libmexclass::error::Error>.

Here's an example of this workflow in example/proxy/Car.cpp:

libmexclass::proxy::MakeResult make(const libmexclass::proxy::FunctionArguments& constructor_arguments) {
     const std::string make  = convert(constructor_arguments, 0);
     const std::string model = convert(constructor_arguments, 1);
     const std::string color = convert(constructor_arguments, 2);

    // return an error if one of the strings is empty
    if (make.empty() || model.empty() || color.empty()) {
        return libmexclass::error::Error{"libmexclass:example:Car:EmptyInputs", "Inputs make, model, and color must be nonempty"};
    }

   // otherwise construct the proxy class and return it
    return std::make_shared<example::proxy::Car>(make, model, color);
}
  1. Change the return type of libmexclass::proxy::Factory::make_proxy() from void to libmexclass::proxy::MakeResult

  2. In the Create::execute() update the code to check if the variant returned contains a std::shared_ptr<libmexclass::proxy::Proxy> or a libmexclass::error::Error. If it has a libmexclass::error::Error, return the error. Otherwise, add the proxy to the manager and return a nullopt.

OLD DESCRIPTION

To enable error-handling, we should consider making all the methods of ProxyManager return some sort of Result or Status object. The callers of these functions (libmexclasss::action::Create, libmexclass::action::Destroy) would be responsible for throwing exceptions in MATLAB.

The advantage of this approach is that ProxyManager would not need to know anything about the MATLABEngine.