Skip to content
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
36 changes: 14 additions & 22 deletions metatomic-core/include/metatomic/errors.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,15 @@ namespace metatomic {
inline void check_status(mta_status_t status) {
if (status == MTA_SUCCESS) {
return;
} else if (status == MTA_UNSUPPORTED_MODEL_ERROR) {
const char* message = nullptr;
const char* origin = nullptr;
void* data = nullptr;
mta_last_error(&message, &origin, &data);
if (origin != nullptr &&std::strcmp(origin, "C++ exception") == 0 && data != nullptr) {
std::rethrow_exception(*static_cast<std::exception_ptr*>(data));
} else {
throw Error(message == nullptr ? "unknown error" : message);
}
}

const char* message = nullptr;
const char* origin = nullptr;
void* data = nullptr;
mta_last_error(&message, &origin, &data);
if (origin != nullptr && std::strcmp(origin, "C++ exception") == 0 && data != nullptr) {
std::rethrow_exception(*static_cast<std::exception_ptr*>(data));
} else {
const char* message = nullptr;
mta_last_error(&message, nullptr, nullptr);
throw Error(message == nullptr ? "unknown error" : message);
}
}
Expand All @@ -54,7 +50,7 @@ namespace metatomic {
} catch (...) {
auto* exception_ptr = new std::exception_ptr(std::current_exception());

const char* message = nullptr;
std::string message;
try {
std::rethrow_exception(*exception_ptr);
} catch (const std::exception& e) {
Expand All @@ -64,7 +60,7 @@ namespace metatomic {
}

auto status = mta_set_last_error(
message,
message.c_str(),
"C++ exception",
exception_ptr,
[](void *ptr) { delete static_cast<std::exception_ptr*>(ptr); }
Expand All @@ -75,15 +71,11 @@ namespace metatomic {
// but we should still try to report the original error
// message if possible.
std::fprintf(stderr, "INTERNAL ERROR: unable to set last error after C++ callback failure (status: %d). ", status);
if (message != nullptr) {
fprintf(stderr, "C++ error was: %s\n", message);
} else {
fprintf(stderr, "Unknown C++ error\n");
}
std::fprintf(stderr, "C++ error was: %s\n", message.c_str());
delete exception_ptr;
}

return MTA_UNSUPPORTED_MODEL_ERROR;
return MTA_MODEL_ERROR;
}
}

Expand All @@ -96,10 +88,10 @@ namespace metatomic {
const char* origin = nullptr;
void* data = nullptr;
mta_last_error(&message, &origin, &data);
if (std::strcmp(origin, "C++ exception") == 0 && data != nullptr) {
if (origin != nullptr && std::strcmp(origin, "C++ exception") == 0 && data != nullptr) {
std::rethrow_exception(*static_cast<std::exception_ptr*>(data));
} else {
throw Error(message);
throw Error(message == nullptr ? "unknown error" : message);
}
}
}
Expand Down
135 changes: 120 additions & 15 deletions metatomic-core/include/metatomic/plugin.hpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
#pragma once

#include <map>
#include <memory>
#include <string>
#include <optional>

#include <nlohmann/json.hpp>

#include <metatomic.h>
#include <metatomic/errors.hpp>
#include <metatomic/model.hpp>

namespace metatomic {
/// Load the shared library at `path` and register the plugin contained
Expand All @@ -12,16 +18,15 @@ namespace metatomic {
///
/// @param path path to the plugin shared library
inline void load_plugin(const std::string& path) {
auto status = mta_load_plugin(path.c_str());
details::check_status(status);
details::check_status(mta_load_plugin(path.c_str()));
}

/// Load a model from `load_from` with the given options.
///
/// If `plugin_name` is empty, metatomic will try to determine the correct
/// plugin to use by checking the `load_from` parameter. If we can not
/// determine the correct plugin, we then try to load the model with each
/// registered plugin until one succeeds.
/// If `plugin_name` is `std::nullopt`, metatomic will try to determine the
/// correct plugin to use by checking the `load_from` parameter. If we can
/// not determine the correct plugin, we then try to load the model with
/// each registered plugin until one succeeds.
///
/// If `plugin_name` is given, then we only try to load the model with the
/// specified plugin, and return an error if the plugin can not load the
Expand All @@ -30,22 +35,122 @@ namespace metatomic {
/// @param load_from where to load the model from (e.g. a file path, a
/// model name, etc.)
/// @param plugin_name optional name of the plugin to use for loading the
/// model, or empty to let metatomic search
/// model, or `std::nullopt` to let metatomic search
/// @param options_json optional JSON object containing string keys and
/// string values for loading the model
/// @return the loaded model
inline mta_model_t load_model(
inline ExternalModel load_model(
const std::string& load_from,
const std::string& options_json = "",
const std::string& plugin_name = ""
std::optional<std::string> options_json = std::nullopt,
std::optional<std::string> plugin_name = std::nullopt
) {
mta_model_t model;
const char* plugin_name_ptr = plugin_name.empty() ? nullptr : plugin_name.c_str();
const char* options_json_ptr = options_json.empty() ? nullptr : options_json.c_str();
const char* plugin_name_ptr = nullptr;
if (plugin_name.has_value()) {
plugin_name_ptr = plugin_name->c_str();
}

const char* options_json_ptr = nullptr;
if (options_json.has_value()) {
options_json_ptr = options_json->c_str();
}

auto status = mta_load_model(load_from.c_str(), options_json_ptr, plugin_name_ptr, &model);
details::check_status(status);
details::check_status(mta_load_model(
load_from.c_str(),
options_json_ptr,
plugin_name_ptr,
&model
));

return model;
return ExternalModel(model);
}

namespace details {
using load_model_t = std::unique_ptr<BaseModel> (*)(
const std::string& load_from,
const std::map<std::string, std::string>& options
);

/// `load_model` callback for a `mta_plugin_t` built by `MTA_REGISTER_CXX_PLUGIN`
inline mta_status_t cxx_plugin_load_model(
const char* load_from,
const char* options_json,
mta_model_t* model,
load_model_t load_model_fn
) {
std::unique_ptr<BaseModel> cpp_model;
auto status = metatomic::details::catch_exceptions([&]() {
auto options_obj = nlohmann::json::parse(options_json);
auto options = options_obj.get<std::map<std::string, std::string>>();
cpp_model = load_model_fn(std::string(load_from), options);
});

if (status != MTA_SUCCESS) {
// exception was caught while trying to load the model
// stop the plugin search and report the error
return status;
}

if (cpp_model == nullptr) {
// the plugin could not load this model
return MTA_UNSUPPORTED_MODEL_ERROR;
}

*model = BaseModel::to_mta_model(std::move(cpp_model));
return MTA_SUCCESS;
}
} // namespace details
} // namespace metatomic


#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-macros"
#endif


/// Plugin entry point for a C++ plugin.
///
/// This should be used once in a C++ plugin shared library. `plugin_name` is
/// the name of the plugin, and `load_model_fn` must be a plain function with
/// the signature:
///
/// ```cpp
/// std::unique_ptr<metatomic::BaseModel> load_model_fn(
/// const std::string& load_from,
/// const std::map<std::string, std::string>& options
/// );
/// ```
///
/// `load_model_fn` MUST return `nullptr` if it can not load the model
/// described by `load_from`, so metatomic can try another plugin.
///
/// Only one `MTA_REGISTER_CXX_PLUGIN` can be used per shared library.
#define MTA_REGISTER_CXX_PLUGIN(plugin_name, load_model_fn) \
MTA_REGISTER_PLUGIN(register_fn, { \
static_assert( \
std::is_convertible< \
decltype(load_model_fn), metatomic::details::load_model_t \
>::value, \
"MTA_REGISTER_CXX_PLUGIN: load_model_fn must be callable as " \
"std::unique_ptr<metatomic::BaseModel>(" \
"std::string, std::map<std::string, std::string>)" \
); \
\
static mta_plugin_t PLUGIN { \
/*abi_version*/ MTA_ABI_VERSION, \
/*name*/ plugin_name, \
/*load_model*/ []( \
const char* load_from, const char* options_json, mta_model_t* model \
) { \
return metatomic::details::cxx_plugin_load_model( \
load_from, options_json, model, load_model_fn \
); \
} \
}; \
return register_fn(PLUGIN); \
})

#ifdef __clang__
#pragma clang diagnostic pop
#endif
54 changes: 54 additions & 0 deletions metatomic-core/tests/cxx/model.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <cstring>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>

Expand Down Expand Up @@ -323,3 +324,56 @@ TEST_CASE("mta_model_view does not take ownership") {
CHECK(model_view_external.unload == nullptr);
CHECK(raw->unload != nullptr);
}

class ThrowingModel: public metatomic::BaseModel {
public:
[[noreturn]] metatomic::ModelCapabilities capabilities() const final {
throw std::out_of_range("ThrowingCppModel: intentional failure in capabilities");
}

metatomic::ModelMetadata metadata() const final {
return metatomic::ModelMetadata::builder()
.name("throwing C++ model")
.build();
}

std::vector<metatomic::PairListOptions> requested_pair_lists() const final {
return {};
}

std::vector<metatomic::Quantity> requested_inputs() const final {
return {};
}

std::vector<metatensor::TensorMap> execute_inner(
const std::vector<metatomic::System>&,
const metatensor::Labels*,
const std::vector<metatomic::Quantity>&
) final {
return {};
}
};


TEST_CASE("C++ exception") {
auto raw = metatomic::BaseModel::to_mta_model(
std::make_unique<ThrowingModel>()
);

// An exception thrown by a C++ model is reported as `MTA_MODEL_ERROR`
mta_string_t capabilities_json = nullptr;
auto status = raw.capabilities(raw.data, &capabilities_json);
CHECK(status == MTA_MODEL_ERROR);
CHECK(status != MTA_UNSUPPORTED_MODEL_ERROR);
CHECK(capabilities_json == nullptr);

const char* message = nullptr;
const char* origin = nullptr;
mta_last_error(&message, &origin, nullptr);
CHECK(std::string(origin) == "C++ exception");
CHECK(std::string(message) == "ThrowingCppModel: intentional failure in capabilities");

// Going back through the C++ API rethrows the original exception
auto model = metatomic::ExternalModel(raw);
CHECK_THROWS_AS(model.capabilities(), std::out_of_range);
}
47 changes: 46 additions & 1 deletion metatomic-core/tests/cxx/plugins.cpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
#include <stdexcept>

#include <catch.hpp>

#include "metatomic.hpp"


TEST_CASE("Load plugins") {
TEST_CASE("Load C plugins") {
metatomic::load_plugin(PLUGIN_DIR "/test-c-plugin.so");

REQUIRE_THROWS_WITH(
Expand All @@ -23,3 +25,46 @@ TEST_CASE("Load plugins") {
"plugin ABI version is 2, but metatomic expects 1"
);
}


TEST_CASE("Load C++ plugins") {
metatomic::load_plugin(PLUGIN_DIR "/test-cxx-plugin.so");

auto model = metatomic::load_model("test-cxx-model", "{}", "test-cxx-plugin");

auto metadata = model.metadata();
CHECK(metadata.name() == "simple C++ plugin model");

auto capabilities = model.capabilities();
CHECK(capabilities.length_unit() == "nm");
REQUIRE(capabilities.outputs().size() == 1);
CHECK(capabilities.outputs()[0].name() == "energy");

REQUIRE_THROWS_WITH(
metatomic::load_model("unknown", "{}", "test-cxx-plugin"),
"invalid parameter: failed to load model from 'unknown': plugin "
"'test-cxx-plugin' could not load the model"
);

// an exception thrown by the plugin is reported as `MTA_MODEL_ERROR`,
// the actual exception makes it back to the caller
REQUIRE_THROWS_WITH(
metatomic::load_model("throws", "{}", "test-cxx-plugin"),
"load_model_cxx: intentional failure for 'throws'"
);
CHECK_THROWS_AS(
metatomic::load_model("throws", "{}", "test-cxx-plugin"),
metatomic::Error
);

CHECK_THROWS_AS(
metatomic::load_model("throws-std", "{}", "test-cxx-plugin"),
std::out_of_range
);

REQUIRE_THROWS_WITH(
metatomic::load_model("unknown"),
Catch::Contains("tried the following plugins, but none could load the model")
&& Catch::Contains("test-cxx-plugin")
);
}
11 changes: 10 additions & 1 deletion metatomic-core/tests/test-plugins/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# create test plugins with a consistent name across platforms
# .so suffix is used for all the platforms

add_library(test-c-plugin SHARED plugin.c)
target_link_libraries(test-c-plugin metatomic)
# create test plugins with a consistent name across platforms
set_target_properties(test-c-plugin PROPERTIES
PREFIX ""
SUFFIX ".so"
Expand All @@ -12,3 +14,10 @@ set_target_properties(bad-abi-plugin PROPERTIES
PREFIX ""
SUFFIX ".so"
)

add_library(test-cxx-plugin SHARED cxx-plugin.cpp)
target_link_libraries(test-cxx-plugin metatomic)
set_target_properties(test-cxx-plugin PROPERTIES
PREFIX ""
SUFFIX ".so"
)
Loading
Loading