diff --git a/runtime/backend/options.h b/runtime/backend/options.h new file mode 100644 index 00000000000..7ff742f4e8f --- /dev/null +++ b/runtime/backend/options.h @@ -0,0 +1,208 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once +#include +#include +#include +#include +#include +#include + +namespace executorch { +namespace runtime { + +static constexpr size_t kMaxOptionKeyLength = 64; +static constexpr size_t kMaxOptionValueLength = 256; + +// All string keys and values must have static storage duration (string +// literals, static const char arrays, or global constants). The BackendOptions +// class does NOT take ownership of strings. +using OptionValue = + std::variant>; + +struct BackendOption { + // key is the name of the backend option, like num_threads, enable_profiling, + // etc + char key[kMaxOptionKeyLength]{}; + // value is the value of the backend option, like 4, true, etc + OptionValue value; +}; + +/** + * A template class for storing and managing backend-specific configuration + * options. + * + * This class provides a type-safe way to store key-value pairs for backend + * configuration, with compile-time capacity limits and runtime type checking. + * It supports bool, int, and const char* value types. + * + * @tparam MaxCapacity The maximum number of options that can be stored + */ +template +class BackendOptions { + public: + /** + * Copy constructor + */ + BackendOptions(const BackendOptions& other) : size_(other.size_) { + for (size_t i = 0; i < size_; ++i) { + options_[i] = other.options_[i]; + } + } + + /** + * Copy assignment operator + */ + BackendOptions& operator=(const BackendOptions& other) { + if (this != &other) { + size_ = other.size_; + for (size_t i = 0; i < size_; ++i) { + options_[i] = other.options_[i]; + } + } + return *this; + } + + /** + * Default constructor - initializes with zero options. + */ + BackendOptions() : size_(0) {} + + /** + * Returns a mutable view of all stored options as a Span. + * + * @return A mutable Span containing all BackendOption entries + */ + executorch::runtime::Span view() { + return executorch::runtime::Span(options_, size_); + } + + /** + * Sets a boolean option value for the given key. + * If the key already exists, updates its value. Otherwise, adds a new option. + * + * @tparam N The length of the key string (automatically deduced) + * @param key The option key (must be a string literal or array) + * @param value The boolean value to set + * @return Error::Ok on success, Error::InvalidArgument if storage is full + */ + template + Error set_option(const char (&key)[N], bool value) noexcept { + static_assert(N <= kMaxOptionKeyLength, "Option key is too long"); + return set_option_impl(key, value); + } + + /** + * Sets an integer option value for the given key. + * If the key already exists, updates its value. Otherwise, adds a new option. + * + * @tparam N The length of the key string (automatically deduced) + * @param key The option key (must be a string literal or array) + * @param value The integer value to set + * @return Error::Ok on success, Error::InvalidArgument if storage is full + */ + template + Error set_option(const char (&key)[N], int value) noexcept { + static_assert(N <= kMaxOptionKeyLength, "Option key is too long"); + return set_option_impl(key, value); + } + + /** + * Sets a string option value for the given key. + * If the key already exists, updates its value. Otherwise, adds a new option. + * + * Note: The string value must have static storage duration. This class does + * NOT take ownership of the string - it only stores the pointer. + * + * @tparam N The length of the key string (automatically deduced) + * @param key The option key (must be a string literal or array) + * @param value The string value to set (must have static storage duration) + * @return Error::Ok on success, Error::InvalidArgument if storage is full + */ + template + Error set_option(const char (&key)[N], const char* value) noexcept { + static_assert(N <= kMaxOptionKeyLength, "Option key is too long"); + // Create a fixed-size array and copy the string + std::array arr{}; + strncpy(arr.data(), value, kMaxOptionValueLength - 1); + arr[kMaxOptionValueLength - 1] = '\0'; // Ensure null termination + return set_option_impl(key, arr); + } + /** + * Retrieves an option value by key and type. + * + * @tparam T The expected type of the option value (bool, int, or const char*) + * @tparam KeyLen The length of the key string (automatically deduced) + * @param key The option key to look up + * @param out Reference to store the retrieved value + * @return Error::Ok if found and type matches, Error::NotFound if key doesn't + * exist, Error::InvalidArgument if type doesn't match + */ + template + Error get_option(const char (&key)[KeyLen], T& out) const { + static_assert(KeyLen <= kMaxOptionKeyLength, "Option key is too long"); + for (size_t i = 0; i < size_; ++i) { + if (std::strcmp(options_[i].key, key) == 0) { + // Special handling for string (convert array to const char*) + if constexpr (std::is_same_v) { + if (auto* arr = std::get_if>( + &options_[i].value)) { + out = arr->data(); // Return pointer to stored array + return Error::Ok; + } + } + // Default handling for bool/int + else if (auto* val = std::get_if(&options_[i].value)) { + out = *val; + return Error::Ok; + } + return Error::InvalidArgument; + } + } + return Error::NotFound; + } + + private: + BackendOption options_[MaxCapacity]{}; // Storage for backend options + size_t size_; // Current number of options + + /** + * Internal implementation for setting option values. + * Handles both updating existing options and adding new ones. + * + * @tparam T The type of the value (bool, int, or const char*) + * @param key The option key + * @param value The value to set + * @return Error::Ok on success, Error::InvalidArgument if storage is full + */ + template + Error set_option_impl(const char* key, T value) { + // Update existing if found + for (size_t i = 0; i < size_; ++i) { + if (strcmp(options_[i].key, key) == 0) { + options_[i].value = value; + return Error::Ok; + } + } + if (size_ < MaxCapacity) { + BackendOption new_option; + const size_t key_len = std::strlen(key); + const size_t copy_len = std::min(key_len, kMaxOptionKeyLength - 1); + std::memcpy(new_option.key, key, copy_len); + new_option.key[copy_len] = '\0'; + new_option.value = value; // Restored value assignment + options_[size_++] = new_option; // Store option and increment size + return Error::Ok; + } + return Error::InvalidArgument; + } +}; + +} // namespace runtime +} // namespace executorch diff --git a/runtime/backend/targets.bzl b/runtime/backend/targets.bzl index d2187afb5fc..93bc85d014f 100644 --- a/runtime/backend/targets.bzl +++ b/runtime/backend/targets.bzl @@ -17,6 +17,7 @@ def define_common_targets(): exported_headers = [ "backend_execution_context.h", "backend_init_context.h", + "options.h", "interface.h", ], preprocessor_flags = ["-DUSE_ATEN_LIB"] if aten_mode else [], diff --git a/runtime/backend/test/backend_options_test.cpp b/runtime/backend/test/backend_options_test.cpp new file mode 100644 index 00000000000..313cac6f143 --- /dev/null +++ b/runtime/backend/test/backend_options_test.cpp @@ -0,0 +1,164 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include +#include + +#include + +using namespace ::testing; +using executorch::runtime::BackendOptions; +using executorch::runtime::Error; + +class BackendOptionsTest : public ::testing::Test { + protected: + void SetUp() override { + // Since these tests cause ET_LOG to be called, the PAL must be initialized + // first. + executorch::runtime::runtime_init(); + } + BackendOptions<5> options; // Capacity of 5 for testing limits +}; + +// Test basic string functionality +TEST_F(BackendOptionsTest, HandlesStringOptions) { + // Set and retrieve valid string + options.set_option("backend_type", "GPU"); + const char* result = nullptr; + EXPECT_EQ(options.get_option("backend_type", result), Error::Ok); + EXPECT_STREQ(result, "GPU"); + + // Update existing key + options.set_option("backend_type", "CPU"); + EXPECT_EQ(options.get_option("backend_type", result), Error::Ok); + EXPECT_STREQ(result, "CPU"); +} + +// Test boolean options +TEST_F(BackendOptionsTest, HandlesBoolOptions) { + options.set_option("debug", true); + bool debug = false; + EXPECT_EQ(options.get_option("debug", debug), Error::Ok); + EXPECT_TRUE(debug); + + // Test false value + options.set_option("verbose", false); + EXPECT_EQ(options.get_option("verbose", debug), Error::Ok); + EXPECT_FALSE(debug); +} + +// Test integer options +TEST_F(BackendOptionsTest, HandlesIntOptions) { + options.set_option("num_threads", 256); + int num_threads = 0; + EXPECT_EQ(options.get_option("num_threads", num_threads), Error::Ok); + EXPECT_EQ(num_threads, 256); +} + +// Test error conditions +TEST_F(BackendOptionsTest, HandlesErrors) { + // Non-existent key + bool dummy_bool; + EXPECT_EQ(options.get_option("missing", dummy_bool), Error::NotFound); + + // Type mismatch + options.set_option("threshold", 100); + const char* dummy_str = nullptr; + EXPECT_EQ(options.get_option("threshold", dummy_str), Error::InvalidArgument); + + // Null value handling, should expect failure + ET_EXPECT_DEATH( + options.set_option("nullable", static_cast(nullptr)), ""); +} + +// Test type-specific keys +TEST_F(BackendOptionsTest, EnforcesKeyTypes) { + // Same key name - later set operations overwrite earlier ones + options.set_option("flag", true); + options.set_option("flag", 123); // Overwrites the boolean entry + + bool bval; + int ival; + + // Boolean get should fail - type was overwritten to INT + EXPECT_EQ(options.get_option("flag", bval), Error::InvalidArgument); + + // Integer get should succeed with correct value + EXPECT_EQ(options.get_option("flag", ival), Error::Ok); + EXPECT_EQ(ival, 123); +} + +TEST_F(BackendOptionsTest, MutableOption) { + int ival; + options.set_option("flag", 0); + // Integer get should succeed with correct value + EXPECT_EQ(options.get_option("flag", ival), Error::Ok); + EXPECT_EQ(ival, 0); + + options.view()[0].value = 123; // Overwrites the entry + + // Integer get should succeed with the updated value + EXPECT_EQ(options.get_option("flag", ival), Error::Ok); + EXPECT_EQ(ival, 123); +} + +// Test copy constructor +TEST_F(BackendOptionsTest, CopyConstructor) { + // Set up original option + options.set_option("debug", true); + + // Create copy using copy constructor + BackendOptions<5> copied_options(options); + + // Verify option was copied correctly + bool debug_val; + EXPECT_EQ(copied_options.get_option("debug", debug_val), Error::Ok); + EXPECT_TRUE(debug_val); + + // Verify independence - modifying original doesn't affect copy + options.set_option("debug", false); + EXPECT_EQ(copied_options.get_option("debug", debug_val), Error::Ok); + EXPECT_TRUE(debug_val); // Should still be true in copy + + // Verify independence - modifying copy doesn't affect original + copied_options.set_option("debug", false); + EXPECT_EQ(options.get_option("debug", debug_val), Error::Ok); + EXPECT_FALSE(debug_val); // Should be false in original +} + +// Test copy assignment operator +TEST_F(BackendOptionsTest, CopyAssignmentOperator) { + // Set up original option + options.set_option("enable_profiling", true); + + // Create another options object and assign to it + BackendOptions<5> assigned_options; + assigned_options.set_option("temp_option", false); // Add something first + + assigned_options = options; + + // Verify option was copied correctly + bool profiling_val; + EXPECT_EQ( + assigned_options.get_option("enable_profiling", profiling_val), + Error::Ok); + EXPECT_TRUE(profiling_val); + + // Verify the temp_option was overwritten (not present in assigned object) + bool temp_val; + EXPECT_EQ( + assigned_options.get_option("temp_option", temp_val), Error::NotFound); + + // Verify independence - modifying original doesn't affect assigned copy + options.set_option("enable_profiling", false); + EXPECT_EQ( + assigned_options.get_option("enable_profiling", profiling_val), + Error::Ok); + EXPECT_TRUE(profiling_val); // Should still be true in assigned copy +} diff --git a/runtime/backend/test/targets.bzl b/runtime/backend/test/targets.bzl index 9ea585f650c..916fa3a3b98 100644 --- a/runtime/backend/test/targets.bzl +++ b/runtime/backend/test/targets.bzl @@ -1,7 +1,17 @@ +load("@fbsource//xplat/executorch/build:runtime_wrapper.bzl", "runtime") + def define_common_targets(): """Defines targets that should be shared between fbcode and xplat. The directory containing this targets.bzl file should also contain both TARGETS and BUCK files that call this function. """ - pass + runtime.cxx_test( + name = "backend_options_test", + srcs = ["backend_options_test.cpp"], + deps = [ + "//executorch/runtime/core:core", + "//executorch/runtime/backend:interface", + "//executorch/test/utils:utils", + ], + )