#ifndef GENMC_MIRI_INTERFACE_HPP #define GENMC_MIRI_INTERFACE_HPP // CXX.rs generated headers: #include "rust/cxx.h" // GenMC generated headers: #include "config.h" // Miri `genmc-sys/src_cpp` headers: #include "ResultHandling.hpp" // GenMC headers: #include "ExecutionGraph/EventLabel.hpp" #include "Static/ModuleID.hpp" #include "Support/MemOrdering.hpp" #include "Support/RMWOps.hpp" #include "Verification/Config.hpp" #include "Verification/GenMCDriver.hpp" // C++ headers: #include #include #include #include /**** Types available to both Rust and C++ ****/ struct GenmcParams; enum class LogLevel : std::uint8_t; struct GenmcScalar; struct SchedulingResult; struct EstimationResult; struct LoadResult; struct StoreResult; struct ReadModifyWriteResult; struct CompareExchangeResult; // GenMC uses `int` for its thread IDs. using ThreadId = int; /// Set the log level for GenMC. /// /// # Safety /// /// This function is not thread safe, since it writes to the global, mutable, non-atomic `logLevel` /// variable. Any GenMC function may read from `logLevel` unsynchronized. /// The safest way to use this function is to set the log level exactly once before first calling /// `create_handle`. /// Never calling this function is safe, GenMC will fall back to its default log level. /* unsafe */ void set_log_level_raw(LogLevel log_level); struct MiriGenmcShim : private GenMCDriver { public: MiriGenmcShim(std::shared_ptr conf, Mode mode /* = VerificationMode{} */) : GenMCDriver(std::move(conf), nullptr, mode) {} /// Create a new `MiriGenmcShim`, which wraps a `GenMCDriver`. /// /// # Safety /// /// This function is marked as unsafe since the `logLevel` global variable is non-atomic. /// This function should not be called in an unsynchronized way with `set_log_level_raw`, /// since this function and any methods on the returned `MiriGenmcShim` may read the /// `logLevel`, causing a data race. The safest way to use these functions is to call /// `set_log_level_raw` once, and only then start creating handles. There should not be any /// other (safe) way to create a `MiriGenmcShim`. /* unsafe */ static auto create_handle(const GenmcParams& params, bool estimation_mode) -> std::unique_ptr; virtual ~MiriGenmcShim() {} /**** Execution start/end handling ****/ // This function must be called at the start of any execution, before any events are // reported to GenMC. void handle_execution_start(); // This function must be called at the end of any execution, even if an error was found // during the execution. // Returns `null`, or a string containing an error message if an error occured. std::unique_ptr handle_execution_end(); /***** Functions for handling events encountered during program execution. *****/ /**** Memory access handling ****/ [[nodiscard]] LoadResult handle_load( ThreadId thread_id, uint64_t address, uint64_t size, MemOrdering ord, GenmcScalar old_val ); [[nodiscard]] ReadModifyWriteResult handle_read_modify_write( ThreadId thread_id, uint64_t address, uint64_t size, RMWBinOp rmw_op, MemOrdering ordering, GenmcScalar rhs_value, GenmcScalar old_val ); [[nodiscard]] CompareExchangeResult handle_compare_exchange( ThreadId thread_id, uint64_t address, uint64_t size, GenmcScalar expected_value, GenmcScalar new_value, GenmcScalar old_val, MemOrdering success_ordering, MemOrdering fail_load_ordering, bool can_fail_spuriously ); [[nodiscard]] StoreResult handle_store( ThreadId thread_id, uint64_t address, uint64_t size, GenmcScalar value, GenmcScalar old_val, MemOrdering ord ); void handle_fence(ThreadId thread_id, MemOrdering ord); /**** Memory (de)allocation ****/ auto handle_malloc(ThreadId thread_id, uint64_t size, uint64_t alignment) -> uint64_t; void handle_free(ThreadId thread_id, uint64_t address); /**** Thread management ****/ void handle_thread_create(ThreadId thread_id, ThreadId parent_id); void handle_thread_join(ThreadId thread_id, ThreadId child_id); void handle_thread_finish(ThreadId thread_id, uint64_t ret_val); void handle_thread_kill(ThreadId thread_id); /***** Exploration related functionality *****/ /** Ask the GenMC scheduler for a new thread to schedule and return whether the execution is * finished, blocked, or can continue. * Updates the next instruction kind for the given thread id. */ auto schedule_next(const int curr_thread_id, const ActionKind curr_thread_next_instr_kind) -> SchedulingResult; /** * Check whether there are more executions to explore. * If there are more executions, this method prepares for the next execution and returns * `true`. Returns true if there are no more executions to explore. */ auto is_exploration_done() -> bool { return GenMCDriver::done(); } /**** Result querying functionality. ****/ // NOTE: We don't want to share the `VerificationResult` type with the Rust side, since it // is very large, uses features that CXX.rs doesn't support and may change as GenMC changes. // Instead, we only use the result on the C++ side, and only expose these getter function to // the Rust side. // Note that CXX.rs doesn't support returning a C++ string to Rust by value, // it must be behind an indirection like a `unique_ptr` (tested with CXX 1.0.170). /// Get the number of blocked executions encountered by GenMC (cast into a fixed with /// integer) auto get_blocked_execution_count() const -> uint64_t { return static_cast(getResult().exploredBlocked); } /// Get the number of executions explored by GenMC (cast into a fixed with integer) auto get_explored_execution_count() const -> uint64_t { return static_cast(getResult().explored); } /// Get all messages that GenMC produced (errors, warnings), combined into one string. auto get_result_message() const -> std::unique_ptr { return std::make_unique(getResult().message); } /// If an error occurred, return a string describing the error, otherwise, return `nullptr`. auto get_error_string() const -> std::unique_ptr { const auto& result = GenMCDriver::getResult(); if (result.status.has_value()) return format_error(result.status.value()); return nullptr; } /**** Printing and estimation mode functionality. ****/ /// Get the results of a run in estimation mode. auto get_estimation_results() const -> EstimationResult; private: /** Increment the event index in the given thread by 1 and return the new event. */ [[nodiscard]] inline auto inc_pos(ThreadId tid) -> Event { ERROR_ON(tid >= threads_action_.size(), "ThreadId out of bounds"); return ++threads_action_[tid].event; } /** Decrement the event index in the given thread by 1 and return the new event. */ inline auto dec_pos(ThreadId tid) -> Event { ERROR_ON(tid >= threads_action_.size(), "ThreadId out of bounds"); return --threads_action_[tid].event; } /** * Helper function for loads that need to reset the event counter when no value is returned. * Same syntax as `GenMCDriver::handleLoad`, but this takes a thread id instead of an Event. * Automatically calls `inc_pos` and `dec_pos` where needed for the given thread. */ template auto handle_load_reset_if_none(ThreadId tid, Ts&&... params) -> HandleResult { const auto pos = inc_pos(tid); const auto ret = GenMCDriver::handleLoad(pos, std::forward(params)...); // If we didn't get a value, we have to reset the index of the current thread. if (!std::holds_alternative(ret)) { dec_pos(tid); } return ret; } /** * GenMC uses the term `Action` to refer to a struct of: * - `ActionKind`, storing whether the next instruction in a thread may be a load * - `Event`, storing the most recent event index added for a thread * * Here we store the "action" for each thread. In particular we use this to assign event * indices, since GenMC expects us to do that. */ std::vector threads_action_; }; /// Get the bit mask that GenMC expects for global memory allocations. /// FIXME(genmc): currently we use `get_global_alloc_static_mask()` to ensure the /// `SAddr::staticMask` constant is consistent between Miri and GenMC, but if /// https://github.com/dtolnay/cxx/issues/1051 is fixed we could share the constant /// directly. constexpr auto get_global_alloc_static_mask() -> uint64_t { return SAddr::staticMask; } // CXX.rs generated headers: // NOTE: this must be included *after* `MiriGenmcShim` and all the other types we use are defined, // otherwise there will be compilation errors due to missing definitions. #include "genmc-sys/src/lib.rs.h" /**** Result handling ****/ // NOTE: these must come after the cxx_bridge generated code, since that contains the actual // definitions of these types. namespace GenmcScalarExt { inline GenmcScalar uninit() { return GenmcScalar { .value = 0, .is_init = false, }; } inline GenmcScalar from_sval(SVal sval) { return GenmcScalar { .value = sval.get(), .is_init = true, }; } inline SVal to_sval(GenmcScalar scalar) { ERROR_ON(!scalar.is_init, "Cannot convert an uninitialized `GenmcScalar` into an `SVal`\n"); return SVal(scalar.value); } } // namespace GenmcScalarExt namespace LoadResultExt { inline LoadResult no_value() { return LoadResult { .error = std::unique_ptr(nullptr), .has_value = false, .read_value = GenmcScalarExt::uninit(), }; } inline LoadResult from_value(SVal read_value) { return LoadResult { .error = std::unique_ptr(nullptr), .has_value = true, .read_value = GenmcScalarExt::from_sval(read_value) }; } inline LoadResult from_error(std::unique_ptr error) { return LoadResult { .error = std::move(error), .has_value = false, .read_value = GenmcScalarExt::uninit() }; } } // namespace LoadResultExt namespace StoreResultExt { inline StoreResult ok(bool is_coherence_order_maximal_write) { return StoreResult { /* error: */ std::unique_ptr(nullptr), is_coherence_order_maximal_write }; } inline StoreResult from_error(std::unique_ptr error) { return StoreResult { .error = std::move(error), .is_coherence_order_maximal_write = false }; } } // namespace StoreResultExt namespace ReadModifyWriteResultExt { inline ReadModifyWriteResult ok(SVal old_value, SVal new_value, bool is_coherence_order_maximal_write) { return ReadModifyWriteResult { .error = std::unique_ptr(nullptr), .old_value = GenmcScalarExt::from_sval(old_value), .new_value = GenmcScalarExt::from_sval(new_value), .is_coherence_order_maximal_write = is_coherence_order_maximal_write }; } inline ReadModifyWriteResult from_error(std::unique_ptr error) { return ReadModifyWriteResult { .error = std::move(error), .old_value = GenmcScalarExt::uninit(), .new_value = GenmcScalarExt::uninit(), .is_coherence_order_maximal_write = false }; } } // namespace ReadModifyWriteResultExt namespace CompareExchangeResultExt { inline CompareExchangeResult success(SVal old_value, bool is_coherence_order_maximal_write) { return CompareExchangeResult { .error = nullptr, .old_value = GenmcScalarExt::from_sval(old_value), .is_success = true, .is_coherence_order_maximal_write = is_coherence_order_maximal_write }; } inline CompareExchangeResult failure(SVal old_value) { return CompareExchangeResult { .error = nullptr, .old_value = GenmcScalarExt::from_sval(old_value), .is_success = false, .is_coherence_order_maximal_write = false }; } inline CompareExchangeResult from_error(std::unique_ptr error) { return CompareExchangeResult { .error = std::move(error), .old_value = GenmcScalarExt::uninit(), .is_success = false, .is_coherence_order_maximal_write = false }; } } // namespace CompareExchangeResultExt #endif /* GENMC_MIRI_INTERFACE_HPP */