diff --git a/.license-tools-config.json b/.license-tools-config.json index fb13be08..ef1ca3ac 100644 --- a/.license-tools-config.json +++ b/.license-tools-config.json @@ -38,6 +38,7 @@ "py\\.typed", "Dockerfile", "prefix_defs.txt", + "client_version.h.in", "test_defs.cpp.in", "LICENSE" ] diff --git a/CHANGELOG.md b/CHANGELOG.md index 517a2bef..5ac93379 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,16 @@ clients compiled against a different minor or major version. _If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#unreleased)._ +### Added + +- ✨ Add a versioned, replaceable Client-driver ABI and a mandatory stable + client-visible device ID ([#511]) ([\@burgholzer]). + ### Changed +- 💥 Make Client session allocation initialize drivers lazily and remove the + example driver's separate initialization and shutdown functions ([#511]) + ([\@burgholzer]). - 💥 Drop x86 macOS from QDMI's tested platforms ([#515]) ([\@denialhaag]) - ⬆️ Raise generated device projects' macOS deployment target to 13.3 ([#515]) ([\@denialhaag]) @@ -229,6 +237,7 @@ for previous changelogs._ [#515]: https://github.com/Munich-Quantum-Software-Stack/QDMI/pull/515 +[#511]: https://github.com/Munich-Quantum-Software-Stack/QDMI/pull/511 [#486]: https://github.com/Munich-Quantum-Software-Stack/QDMI/pull/486 [#485]: https://github.com/Munich-Quantum-Software-Stack/QDMI/pull/485 [#475]: https://github.com/Munich-Quantum-Software-Stack/QDMI/pull/475 diff --git a/CMakeLists.txt b/CMakeLists.txt index f8851cac..4d0dab69 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,7 +20,7 @@ cmake_minimum_required(VERSION 3.24...4.4) project( qdmi LANGUAGES C CXX - VERSION 1.3.4 + VERSION 1.4.0 DESCRIPTION "QDMI –– Quantum Device Management Interface") set(PROJECT_VERSION_PRERELEASE "-dev") @@ -85,6 +85,11 @@ if(NOT USE_INSTALLED_QDMI) set(QDMI_CMAKE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/cmake" CACHE INTERNAL "CMake directory") + set(QDMI_GENERATED_INCLUDE_BUILD_DIR "${PROJECT_BINARY_DIR}/include") + file(MAKE_DIRECTORY "${QDMI_GENERATED_INCLUDE_BUILD_DIR}/qdmi") + configure_file( + "${QDMI_CMAKE_DIR}/client_version.h.in" + "${QDMI_GENERATED_INCLUDE_BUILD_DIR}/qdmi/client_version.h" @ONLY) # make scripts available to cmake list(APPEND CMAKE_MODULE_PATH ${QDMI_CMAKE_DIR}) @@ -114,10 +119,20 @@ if(NOT USE_INSTALLED_QDMI) # collect header files file(GLOB_RECURSE QDMI_HEADERS ${QDMI_INCLUDE_BUILD_DIR}/qdmi/*.h) + set(QDMI_GENERATED_HEADERS + "${QDMI_GENERATED_INCLUDE_BUILD_DIR}/qdmi/client_version.h") # add headers using file sets - target_sources(qdmi PUBLIC FILE_SET HEADERS BASE_DIRS - ${QDMI_INCLUDE_BUILD_DIR} FILES ${QDMI_HEADERS}) + target_sources( + qdmi + PUBLIC FILE_SET + HEADERS + BASE_DIRS + ${QDMI_INCLUDE_BUILD_DIR} + ${QDMI_GENERATED_INCLUDE_BUILD_DIR} + FILES + ${QDMI_HEADERS} + ${QDMI_GENERATED_HEADERS}) # set required C standard target_compile_features(qdmi INTERFACE c_std_11) diff --git a/UPGRADING.md b/UPGRADING.md index 3a53bd94..e608b70b 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -7,6 +7,47 @@ releases, please refer to the ## [Unreleased] +### Replaceable Client drivers and stable device IDs + +QDMI 1.4 defines a stable ABI for replaceable Client driver libraries. Export +every function declared in `qdmi/client.h` with `QDMI_DRIVER_EXPORT`. Define +`QDMI_driver_EXPORTS` while building the driver. A loader first resolves and +calls `QDMI_driver_get_client_abi_version`. It then resolves the complete Client +Interface before it allocates a session. The returned ABI is compatible if and +only if its packed major and minor fields equal those of +`QDMI_CLIENT_ABI_VERSION`. Ignore the patch field when checking compatibility. A +different major or minor field is incompatible. QDMI 1.4 defines +`QDMI_CLIENT_ABI_VERSION` as 1.4.0. CMake derives the ABI version from the QDMI +release version. Device library versions remain independent. + +The ABI version query does not initialize the driver. `QDMI_session_alloc` is +the first stateful Client call. It initializes the driver lazily, sets its +output to `NULL` before work that can fail, and leaves no partial session on +failure. Clients can retry a failed allocation. The example driver no longer +exposes `QDMI_driver_init` or `QDMI_driver_shutdown`. + +A process uses one Client implementation and can allocate many sessions. Each +initialized session exposes an immutable authorized device catalog. Device, +site, operation, and job handles belong to that session. Free all jobs before +freeing the session. Freeing the session invalidates every remaining descendant +handle. + +`QDMI_DEVICE_PROPERTY_ID` is appended as value 18. It is mandatory through the +Client Interface and optional through the Device Interface. A driver supplies +the value when a device returns `QDMI_ERROR_NOTSUPPORTED`. The ID is a nonempty, +opaque string. It is unique within an initialized session, immutable for one +device handle, and stable across equivalent sessions and process restarts while +the same logical resource exists. Persist the driver deployment with the ID. Do +not use a display name, endpoint, pointer, credential, library version, symbol +prefix, or the `QDMI_DEVICE_ID` CMake target property as the runtime ID. + +The example driver configuration now gives each device a runtime ID in a third +column: + +```text +/path/to/libdevice.so PREFIX deployment.device-id +``` + ### macOS support QDMI no longer tests x86 macOS. Generated device projects now target macOS 13.3 diff --git a/cmake/PrefixHandling.cmake b/cmake/PrefixHandling.cmake index 096d45f8..17480393 100644 --- a/cmake/PrefixHandling.cmake +++ b/cmake/PrefixHandling.cmake @@ -74,8 +74,8 @@ function(generate_prefixed_qdmi_headers prefix) endforeach() endfunction() -# Publish the metadata that build-system consumers need to identify a QDMI -# device target. +# Publish the package metadata that build-system consumers need to identify a +# QDMI device target. The ID is not the runtime QDMI_DEVICE_PROPERTY_ID. function(configure_qdmi_device_target) cmake_parse_arguments(ARG "" "TARGET;ID;PREFIX" "" ${ARGN}) foreach(required_argument IN ITEMS TARGET ID PREFIX) diff --git a/cmake/client_version.h.in b/cmake/client_version.h.in new file mode 100644 index 00000000..01cb5f49 --- /dev/null +++ b/cmake/client_version.h.in @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 - 2026 QDMI Maintainers + * All rights reserved. + * + * Licensed under the Apache License v2.0 with LLVM Exceptions (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://llvm.org/LICENSE.txt + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +#ifndef QDMI_CLIENT_VERSION_H +#define QDMI_CLIENT_VERSION_H + +#include "qdmi/constants.h" + +#define QDMI_CLIENT_ABI_VERSION QDMI_MAKE_VERSION(@PROJECT_VERSION_MAJOR@, @PROJECT_VERSION_MINOR@, @PROJECT_VERSION_PATCH@) + +#endif // QDMI_CLIENT_VERSION_H diff --git a/cmake/prefix_defs.txt b/cmake/prefix_defs.txt index 31a67379..e71f7f61 100644 --- a/cmake/prefix_defs.txt +++ b/cmake/prefix_defs.txt @@ -29,8 +29,10 @@ QDMI_Child_Device QDMI_Child_Device_impl_d QDMI_STATIC_DEFINE QDMI_EXPORT +QDMI_DRIVER_EXPORT QDMI_NO_EXPORT QDMI_device_EXPORTS +QDMI_driver_EXPORTS QDMI_DEPRECATED QDMI_DEPRECATED_EXPORT QDMI_DEPRECATED_NO_EXPORT diff --git a/docs/CMakeLists.txt b/docs/CMakeLists.txt index e8bf4a1d..dac4f22a 100644 --- a/docs/CMakeLists.txt +++ b/docs/CMakeLists.txt @@ -22,7 +22,8 @@ set(DOXYGEN_CONFIG_FILE_IN ${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile.in) # Set the input directories for doxygen, i.e., all files that contain source # code with documentation. -set(DOXYGEN_INPUT_DIRS ${PROJECT_SOURCE_DIR}/include) +set(DOXYGEN_INPUT_DIRS ${PROJECT_SOURCE_DIR}/include + ${QDMI_GENERATED_INCLUDE_BUILD_DIR}) # This variable is only needed to setup the dependency tracking of CMake # correctly and is not passed to Doxygen. Whenever such a file is modified, # CMake detects that change and will rerun the Doxygen command instead of using @@ -31,6 +32,7 @@ file( GLOB_RECURSE DOXYGEN_INPUT_FILES ${PROJECT_SOURCE_DIR}/include/*.h + ${QDMI_GENERATED_INCLUDE_BUILD_DIR}/*.h ${CMAKE_CURRENT_SOURCE_DIR}/*.md ${PROJECT_SOURCE_DIR}/CHANGELOG.md ${PROJECT_SOURCE_DIR}/UPGRADING.md diff --git a/docs/Doxyfile.in b/docs/Doxyfile.in index f6882067..43300f6d 100644 --- a/docs/Doxyfile.in +++ b/docs/Doxyfile.in @@ -2473,6 +2473,7 @@ INCLUDE_FILE_PATTERNS = # This tag requires that the tag ENABLE_PREPROCESSING is set to YES. PREDEFINED = "QDMI_EXPORT=" \ + "QDMI_DRIVER_EXPORT=" \ "QDMI_NO_EXPORT=" \ "QDMI_DEPRECATED=" \ "QDMI_DEPRECATED_EXPORT=" \ diff --git a/docs/examples.md b/docs/examples.md index 1e1e4b63..5da6ad84 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -10,6 +10,29 @@ contained in the `examples/` directory in the repository. \tableofcontents +## Implementing a Client Driver {#client-driver} + +A Client driver is a replaceable shared library. It exports every function in +`qdmi/client.h` with `QDMI_DRIVER_EXPORT`, including +`QDMI_driver_get_client_abi_version`. A loader accepts a version if and only if +its major and minor fields equal those of `QDMI_CLIENT_ABI_VERSION`; the patch +field does not affect compatibility. The loader then resolves the complete +Client Interface before it calls `QDMI_session_alloc`. The ABI query is +side-effect free. Session allocation initializes the driver lazily and returns a +null handle on failure. + +The example driver's `QDMI_CONF` file contains one device per line: + +```text +/path/to/libdevice.so PREFIX deployment.device-id +``` + +The third field is the nonempty client-visible `QDMI_DEVICE_PROPERTY_ID`. IDs +must be unique in the configured catalog. The driver reads and validates the +complete file transactionally when it allocates the first session. A failed +allocation can be retried with a corrected file. Device libraries can omit this +property because the Client driver owns the public ID. + ## Implementing a Device {#device} Below you find mock implementations of a QDMI device in C++. diff --git a/docs/templates.md b/docs/templates.md index 74b04f80..0c4bbbfd 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -55,13 +55,13 @@ that. To this end, adjust the `QDMI_REV` variable in + set(QDMI_REV "v1.2.0" ``` -The generated project assigns the stable ID `prefix.default` to its device. -Change the project-specific `PREFIX_QDMI_DEVICE_ID` CMake cache variable if the -device needs a different ID. Once selected and distributed, keep this ID stable -so that applications and configuration files can continue to refer to the same -device. +The generated project assigns the package ID `prefix.default` to its device +target. Change the project-specific `PREFIX_QDMI_DEVICE_ID` CMake cache variable +if the device package needs a different ID. This build-system value identifies a +packaged device implementation. It is not the runtime `QDMI_DEVICE_PROPERTY_ID`, +which a Client driver assigns to a logical resource. -The device target calls `configure_qdmi_device_target` to export its stable ID +The device target calls `configure_qdmi_device_target` to export its package ID and symbol prefix as the `QDMI_DEVICE_ID` and `QDMI_DEVICE_PREFIX` target properties. Build-system consumers such as MQT Core can use this metadata to package and register the device without project-specific loader code or a diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index f7bbe91f..3c3f48c7 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -15,6 +15,7 @@ # # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +set(BUILD_CXX_QDMI_TESTS ${BUILD_QDMI_TESTS}) add_subdirectory(device) add_subdirectory(fomac) add_subdirectory(tool) diff --git a/examples/device/CMakeLists.txt b/examples/device/CMakeLists.txt index 5a2a4486..fb56d3bb 100644 --- a/examples/device/CMakeLists.txt +++ b/examples/device/CMakeLists.txt @@ -53,7 +53,7 @@ set(CMAKE_VERIFY_INTERFACE_HEADER_SETS set(QDMI_PREFIX "CXX") set(CXX_QDMI_DEVICE_ID "cxx.default" - CACHE STRING "Stable identifier for the CXX QDMI Device") + CACHE STRING "Package identifier for the CXX QDMI Device") cmake_dependent_option( INSTALL_CXX_QDMI_DEVICE @@ -86,7 +86,7 @@ if(BUILD_CXX_QDMI_TESTS) get_target_property(EXPORTED_QDMI_DEVICE_PREFIX ${QDMI_TARGET_NAME} QDMI_DEVICE_PREFIX) if(NOT EXPORTED_QDMI_DEVICE_ID STREQUAL "${CXX_QDMI_DEVICE_ID}") - message(FATAL_ERROR "The CXX QDMI target does not export its stable ID") + message(FATAL_ERROR "The CXX QDMI target does not export its package ID") endif() if(NOT EXPORTED_QDMI_DEVICE_PREFIX STREQUAL "${QDMI_PREFIX}") message(FATAL_ERROR "The CXX QDMI target does not export its symbol prefix") diff --git a/examples/device/README.md b/examples/device/README.md index fd20ebab..15e7546e 100644 --- a/examples/device/README.md +++ b/examples/device/README.md @@ -11,11 +11,11 @@ A C++20 library that implements the QDMI Device interface. -The exported CMake target publishes the stable device ID configured through +The exported CMake target publishes the package ID configured through `CXX_QDMI_DEVICE_ID` and the QDMI symbol prefix through `configure_qdmi_device_target`. Consumers such as MQT Core can use this metadata -to package and register the device without project-specific loader code. This -metadata does not add MQT Core as a dependency. +to package and register the device without project-specific loader code. The +package ID is not the runtime `QDMI_DEVICE_PROPERTY_ID`. ## Documentation diff --git a/examples/device/test/test_cxx_device.cpp b/examples/device/test/test_cxx_device.cpp index e147669d..edd21b03 100644 --- a/examples/device/test/test_cxx_device.cpp +++ b/examples/device/test/test_cxx_device.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include class QDMIImplementationTest : public ::testing::Test { @@ -35,6 +36,12 @@ class QDMIImplementationTest : public ::testing::Test { ASSERT_EQ(CXX_QDMI_device_session_alloc(&session), QDMI_SUCCESS) << "Failed to allocate a session"; + constexpr std::string_view token = "token"; + ASSERT_EQ(CXX_QDMI_device_session_set_parameter( + session, QDMI_DEVICE_SESSION_PARAMETER_TOKEN, + token.size() + 1, token.data()), + QDMI_SUCCESS); + ASSERT_EQ(CXX_QDMI_device_session_init(session), QDMI_SUCCESS) << "Failed to initialize a session. Potential errors: Wrong or missing " "authentication information, device status is offline, or in " @@ -42,7 +49,10 @@ class QDMIImplementationTest : public ::testing::Test { << ":" << (__LINE__ - 4); } - void TearDown() override { CXX_QDMI_device_finalize(); } + void TearDown() override { + CXX_QDMI_device_session_free(session); + CXX_QDMI_device_finalize(); + } }; TEST_F(QDMIImplementationTest, SessionSetParameterImplemented) { @@ -170,6 +180,12 @@ TEST_F(QDMIImplementationTest, QueryDeviceVersionImplemented) { ASSERT_FALSE(value.empty()) << "Devices must provide a version"; } +TEST_F(QDMIImplementationTest, ClientVisibleDeviceIdIsDriverOwned) { + EXPECT_EQ(CXX_QDMI_device_session_query_device_property( + session, QDMI_DEVICE_PROPERTY_ID, 0, nullptr, nullptr), + QDMI_ERROR_NOTSUPPORTED); +} + TEST_F(QDMIImplementationTest, QueryDeviceLibraryVersionImplemented) { size_t size = 0; ASSERT_EQ( diff --git a/examples/driver/CMakeLists.txt b/examples/driver/CMakeLists.txt index 1075efb1..17cea103 100644 --- a/examples/driver/CMakeLists.txt +++ b/examples/driver/CMakeLists.txt @@ -18,12 +18,14 @@ # add C++ language support enable_language(CXX) -add_library(qdmi_example_driver qdmi_example_driver.cpp qdmi_example_driver.h) +add_library(qdmi_example_driver SHARED qdmi_example_driver.cpp) target_link_libraries(qdmi_example_driver PRIVATE qdmi::qdmi qdmi::qdmi_project_warnings) -target_include_directories(qdmi_example_driver - PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_compile_definitions(qdmi_example_driver PRIVATE QDMI_driver_EXPORTS) target_compile_features(qdmi_example_driver PRIVATE cxx_std_20) -set_target_properties(qdmi_example_driver PROPERTIES POSITION_INDEPENDENT_CODE - ON) +set_target_properties( + qdmi_example_driver + PROPERTIES CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN 1 + POSITION_INDEPENDENT_CODE ON) add_library(qdmi::example_driver ALIAS qdmi_example_driver) diff --git a/examples/driver/qdmi_example_driver.cpp b/examples/driver/qdmi_example_driver.cpp index a19a4f76..fa9873d3 100644 --- a/examples/driver/qdmi_example_driver.cpp +++ b/examples/driver/qdmi_example_driver.cpp @@ -21,26 +21,27 @@ * @brief An example driver implementation in C++. */ -#include "qdmi_example_driver.h" - #include "qdmi/client.h" #include "qdmi/device.h" #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include #include #include #include +#include #include /** @@ -62,6 +63,8 @@ enum class QDMI_SESSION_STATUS : uint8_t { ALLOCATED, INITIALIZED }; */ struct QDMI_Library { void *lib_handle = nullptr; + std::string device_id; + bool initialized = false; /// Function pointer to @ref QDMI_device_initialize. decltype(QDMI_device_initialize) *device_initialize{}; @@ -119,8 +122,8 @@ struct QDMI_Library { // destructor ~QDMI_Library() { - // Check if QDMI_device_finalize is not NULL before calling it. - if (device_finalize != nullptr) { + // Finalize only after successful initialization. + if (initialized) { device_finalize(); } // close the dynamic library @@ -137,6 +140,18 @@ struct QDMI_Device_impl_d { QDMI_Library *library = nullptr; QDMI_Session session = nullptr; QDMI_Device_Session device_session = nullptr; + + QDMI_Device_impl_d() = default; + QDMI_Device_impl_d(const QDMI_Device_impl_d &) = delete; + QDMI_Device_impl_d &operator=(const QDMI_Device_impl_d &) = delete; + QDMI_Device_impl_d(QDMI_Device_impl_d &&) = delete; + QDMI_Device_impl_d &operator=(QDMI_Device_impl_d &&) = delete; + + ~QDMI_Device_impl_d() { + if (device_session != nullptr) { + library->device_session_free(device_session); + } + } }; /** @@ -159,7 +174,7 @@ struct QDMI_Job_impl_d { struct QDMI_Driver_State { std::unordered_map> libraries; - std::unordered_set sessions; + bool initialized = false; }; namespace { @@ -188,58 +203,53 @@ QDMI_Driver_State *QDMI_get_driver_state() { dlsym((device).lib_handle, symbol_name.c_str())); \ } -void QDMI_library_load(const std::string &lib_name, const std::string &prefix) { - auto *lib_handle = dlopen(lib_name.c_str(), RTLD_NOW | RTLD_LOCAL); - if (lib_handle == nullptr) { +void QDMI_library_load( + std::unordered_map> &libraries, + const std::string &lib_name, const std::string &prefix, + const std::string &device_id) { + auto library = std::make_unique(); + library->lib_handle = dlopen(lib_name.c_str(), RTLD_NOW | RTLD_LOCAL); + if (library->lib_handle == nullptr) { throw std::runtime_error("Couldn't open the device library: " + lib_name); } - auto &libraries = QDMI_get_driver_state()->libraries; - if (const auto it = libraries.find(lib_handle); it != libraries.end()) { - // dlopen uses reference counting, so we need to decrement the reference - // count that was increased by dlopen. - dlclose(lib_handle); - return; - } - auto it = - libraries.emplace(lib_handle, std::make_unique()).first; - auto &library = *it->second; - library.lib_handle = lib_handle; - - try { - // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) - - // load the function symbols from the dynamic library - LOAD_SYMBOL(library, prefix, device_initialize) - LOAD_SYMBOL(library, prefix, device_finalize) - // device session interface - LOAD_SYMBOL(library, prefix, device_session_alloc) - LOAD_SYMBOL(library, prefix, device_session_init) - LOAD_SYMBOL(library, prefix, device_session_free) - LOAD_SYMBOL(library, prefix, device_session_set_parameter) - // device job interface - LOAD_SYMBOL(library, prefix, device_session_create_device_job) - LOAD_OPTIONAL_SYMBOL(library, prefix, - device_session_retrieve_device_job_by_id) - LOAD_SYMBOL(library, prefix, device_job_free) - LOAD_SYMBOL(library, prefix, device_job_set_parameter) - LOAD_SYMBOL(library, prefix, device_job_query_property) - LOAD_SYMBOL(library, prefix, device_job_submit) - LOAD_SYMBOL(library, prefix, device_job_cancel) - LOAD_SYMBOL(library, prefix, device_job_check) - LOAD_SYMBOL(library, prefix, device_job_wait) - LOAD_SYMBOL(library, prefix, device_job_get_results) - // device query interface - LOAD_SYMBOL(library, prefix, device_session_query_device_property) - LOAD_SYMBOL(library, prefix, device_session_query_site_property) - LOAD_SYMBOL(library, prefix, device_session_query_operation_property) - - // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) - } catch (const std::exception &) { - dlclose(lib_handle); - throw; - } - // initialize the device - library.device_initialize(); + if (libraries.contains(library->lib_handle)) { + throw std::runtime_error("Device library is listed more than once: " + + lib_name); + } + library->device_id = device_id; + + // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) + + /// Resolve device symbols before initializing the library. + LOAD_SYMBOL(*library, prefix, device_initialize) + LOAD_SYMBOL(*library, prefix, device_finalize) + LOAD_SYMBOL(*library, prefix, device_session_alloc) + LOAD_SYMBOL(*library, prefix, device_session_init) + LOAD_SYMBOL(*library, prefix, device_session_free) + LOAD_SYMBOL(*library, prefix, device_session_set_parameter) + LOAD_SYMBOL(*library, prefix, device_session_create_device_job) + LOAD_OPTIONAL_SYMBOL(*library, prefix, + device_session_retrieve_device_job_by_id) + LOAD_SYMBOL(*library, prefix, device_job_free) + LOAD_SYMBOL(*library, prefix, device_job_set_parameter) + LOAD_SYMBOL(*library, prefix, device_job_query_property) + LOAD_SYMBOL(*library, prefix, device_job_submit) + LOAD_SYMBOL(*library, prefix, device_job_cancel) + LOAD_SYMBOL(*library, prefix, device_job_check) + LOAD_SYMBOL(*library, prefix, device_job_wait) + LOAD_SYMBOL(*library, prefix, device_job_get_results) + LOAD_SYMBOL(*library, prefix, device_session_query_device_property) + LOAD_SYMBOL(*library, prefix, device_session_query_site_property) + LOAD_SYMBOL(*library, prefix, device_session_query_operation_property) + + // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) + + if (library->device_initialize() != QDMI_SUCCESS) { + throw std::runtime_error("Failed to initialize device library: " + + lib_name); + } + library->initialized = true; + libraries.emplace(library->lib_handle, std::move(library)); } bool Is_path_allowed(const std::filesystem::path &path) { @@ -275,60 +285,78 @@ bool Is_path_allowed(const std::filesystem::path &path) { .first == allowed_path.end(); }); } -} // namespace - -int QDMI_driver_init() { - const char *config_file = std::getenv("QDMI_CONF"); - if (config_file == nullptr) { - config_file = "qdmi.conf"; - } - - // Validate the configuration file path - if (!Is_path_allowed(config_file)) { - std::cerr << "Config file path is not allowed: " << config_file << "\n"; - return QDMI_ERROR_FATAL; - } - - std::ifstream file(config_file); - if (!file.is_open()) { - std::cerr << "Couldn't open the configuration file: " << config_file - << "\n"; - return QDMI_ERROR_FATAL; - } - std::string line; - while (std::getline(file, line)) { - if (line.empty() || line[0] == '#') { - continue; // Skip empty lines and comments +int QDMI_initialize_driver() { + std::unordered_map> libraries; + std::unordered_set device_ids; + try { + const char *config_file = std::getenv("QDMI_CONF"); + if (config_file == nullptr) { + config_file = "qdmi.conf"; } - std::istringstream iss(line); - std::string lib_name; - std::string prefix; - if (!(iss >> lib_name >> prefix)) { - std::cerr << "Invalid configuration line: " << line << "\n"; - continue; + if (!Is_path_allowed(config_file)) { + std::cerr << "Config file path is not allowed: " << config_file << '\n'; + return QDMI_ERROR_FATAL; } - try { - QDMI_library_load(lib_name, prefix); - } catch (const std::exception &e) { - std::cerr << "Couldn't open the device: " << e.what() << "\n"; + std::ifstream file(config_file); + if (!file.is_open()) { + std::cerr << "Couldn't open the configuration file: " << config_file + << '\n'; return QDMI_ERROR_FATAL; } + + std::string line; + while (std::getline(file, line)) { + if (line.empty() || line.front() == '#') { + continue; + } + + std::istringstream iss(line); + std::string lib_name; + std::string prefix; + std::string device_id; + std::string extra; + if (!(iss >> lib_name >> prefix >> device_id) || (iss >> extra) || + !device_ids.emplace(device_id).second) { + std::cerr << "Invalid configuration line: " << line << '\n'; + return QDMI_ERROR_FATAL; + } + QDMI_library_load(libraries, lib_name, prefix, device_id); + } + } catch (const std::bad_alloc &) { + return QDMI_ERROR_OUTOFMEM; + } catch (const std::exception &e) { + std::cerr << "Couldn't initialize the driver: " << e.what() << '\n'; + return QDMI_ERROR_FATAL; } - file.close(); + auto *driver_state = QDMI_get_driver_state(); + driver_state->libraries = std::move(libraries); + driver_state->initialized = true; return QDMI_SUCCESS; } +} // namespace + +uint32_t QDMI_driver_get_client_abi_version() { + return QDMI_CLIENT_ABI_VERSION; +} int QDMI_session_alloc(QDMI_Session *session) { if (session == nullptr) { return QDMI_ERROR_INVALIDARGUMENT; } - *session = new QDMI_Session_impl_d(); - QDMI_get_driver_state()->sessions.emplace(*session); - return QDMI_SUCCESS; + *session = nullptr; + auto *driver_state = QDMI_get_driver_state(); + if (!driver_state->initialized) { + const auto status = QDMI_initialize_driver(); + if (status != QDMI_SUCCESS) { + return status; + } + } + *session = new (std::nothrow) QDMI_Session_impl_d(); + return *session == nullptr ? QDMI_ERROR_OUTOFMEM : QDMI_SUCCESS; } int QDMI_session_init(QDMI_Session session) { @@ -349,34 +377,43 @@ int QDMI_session_init(QDMI_Session session) { ? QDMI_DEVICE_MODE::QDMI_DEVICE_MODE_READONLY : QDMI_DEVICE_MODE::QDMI_SESSION_MODE_READWRITE; - // Create a session for every device and initialize it. - for (const auto &[_, lib] : QDMI_get_driver_state()->libraries) { - auto &device = session->device_list.emplace_back( - std::make_unique()); - device->library = lib.get(); - device->session = session; - // Allocate a device session - device->library->device_session_alloc(&device->device_session); - // Forward the stored token to the device session - device->library->device_session_set_parameter( - device->device_session, QDMI_DEVICE_SESSION_PARAMETER_TOKEN, - session->token->size() + 1, session->token->c_str()); - // Initialize the device session - device->library->device_session_init(device->device_session); - } - session->status = QDMI_SESSION_STATUS::INITIALIZED; - return QDMI_SUCCESS; + try { + std::vector> devices; + for (const auto &[_, lib] : QDMI_get_driver_state()->libraries) { + auto device = std::make_unique(); + device->library = lib.get(); + device->session = session; + auto status = + device->library->device_session_alloc(&device->device_session); + if (status != QDMI_SUCCESS) { + return status; + } + status = device->library->device_session_set_parameter( + device->device_session, QDMI_DEVICE_SESSION_PARAMETER_TOKEN, + session->token->size() + 1, session->token->c_str()); + if (status != QDMI_SUCCESS) { + return status; + } + status = device->library->device_session_init(device->device_session); + if (status != QDMI_SUCCESS) { + return status; + } + devices.emplace_back(std::move(device)); + } + session->device_list = std::move(devices); + session->status = QDMI_SESSION_STATUS::INITIALIZED; + return QDMI_SUCCESS; + } catch (const std::bad_alloc &) { + return QDMI_ERROR_OUTOFMEM; + } catch (const std::exception &) { + return QDMI_ERROR_FATAL; + } } void QDMI_session_free(QDMI_Session session) { if (session == nullptr) { return; } - for (auto &device : session->device_list) { - device->library->device_session_free(device->device_session); - } - session->device_list.clear(); - QDMI_get_driver_state()->sessions.erase(session); delete session; } @@ -442,18 +479,6 @@ int QDMI_session_query_session_property(QDMI_Session session, return QDMI_SUCCESS; } -int QDMI_driver_shutdown() { - auto *driver_state = QDMI_get_driver_state(); - - // Close all open sessions - while (!driver_state->sessions.empty()) { - QDMI_session_free(*driver_state->sessions.begin()); - } - // Close all libraries - driver_state->libraries.clear(); - return QDMI_SUCCESS; -} - int QDMI_device_create_job(QDMI_Device dev, QDMI_Job *job) { if (dev == nullptr || job == nullptr) { return QDMI_ERROR_INVALIDARGUMENT; @@ -570,6 +595,19 @@ int QDMI_device_query_device_property(QDMI_Device device, if (device == nullptr) { return QDMI_ERROR_INVALIDARGUMENT; } + if (prop == QDMI_DEVICE_PROPERTY_ID) { + const auto required_size = device->library->device_id.size() + 1; + if (value != nullptr) { + if (size < required_size) { + return QDMI_ERROR_INVALIDARGUMENT; + } + std::memcpy(value, device->library->device_id.c_str(), required_size); + } + if (size_ret != nullptr) { + *size_ret = required_size; + } + return QDMI_SUCCESS; + } return device->library->device_session_query_device_property( device->device_session, prop, size, value, size_ret); } diff --git a/examples/driver/qdmi_example_driver.h b/examples/driver/qdmi_example_driver.h deleted file mode 100644 index 47927997..00000000 --- a/examples/driver/qdmi_example_driver.h +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (c) 2024 - 2026 QDMI Maintainers - * All rights reserved. - * - * Licensed under the Apache License v2.0 with LLVM Exceptions (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://llvm.org/LICENSE.txt - * - * 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. - * - * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - */ - -/** @file - * @brief An example driver implementation in C++. - */ - -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Initialize the QDMI driver. - * @details This function must be called exactly once before any other QDMI - * function. It performs any necessary initialization of the driver so that a - * client can allocate sessions (@ref QDMI_Session) and access devices (@ref - * QDMI_Device). - * @return @ref QDMI_SUCCESS if the driver was initialized successfully. - * @return @ref QDMI_ERROR_FATAL if an unexpected error occurred. - */ -int QDMI_driver_init(); - -/** - * @brief Shutdown the QDMI driver. - * @details This function must be called exactly once to cleanly shut down the - * driver. It should be called after all sessions have been freed and no more - * QDMI functions will be called. - * @return @ref QDMI_SUCCESS if the driver was shut down successfully. - * @return @ref QDMI_ERROR_FATAL if an unexpected error occurred. - */ -int QDMI_driver_shutdown(); - -#ifdef __cplusplus -} // extern "C" -#endif diff --git a/include/qdmi/client.h b/include/qdmi/client.h index 90ec9bac..7b41814e 100644 --- a/include/qdmi/client.h +++ b/include/qdmi/client.h @@ -23,8 +23,10 @@ #pragma once -#include "qdmi/constants.h" // IWYU pragma: export -#include "qdmi/types.h" // IWYU pragma: export +#include "qdmi/client_version.h" // IWYU pragma: export +#include "qdmi/constants.h" // IWYU pragma: export +#include "qdmi/export.h" // IWYU pragma: export +#include "qdmi/types.h" // IWYU pragma: export #ifdef __cplusplus #include @@ -44,6 +46,16 @@ extern "C" { * It includes functions to establish sessions between a QDMI driver and a * client, as well as to interact with the devices managed by the driver. * + * A process uses one implementation of this interface and can allocate many + * independent sessions from it. A dynamic loader must first resolve and call + * @ref QDMI_driver_get_client_abi_version. A returned ABI is compatible if and + * only if its major and minor fields equal those of @ref + * QDMI_CLIENT_ABI_VERSION. The loader must ignore the patch field for + * compatibility. It must then resolve the complete Client Interface declared + * in this header before it calls @ref QDMI_session_alloc. The driver library + * must remain loaded until every session and every handle obtained from those + * sessions is freed. + * * The client interface is split into three parts: * - The @ref client_session_interface "client session interface" for managing * sessions between a QDMI driver and a client. @@ -55,11 +67,31 @@ extern "C" { * @{ */ +/** + * @def QDMI_CLIENT_ABI_VERSION + * @brief Client Interface ABI implemented by compatible QDMI drivers. + * @details CMake derives this value from the QDMI release version. + */ + +/** + * @brief Return the Client Interface ABI implemented by the driver. + * @details This function has no side effects, is safe to call concurrently, + * and does not initialize global driver state. A returned ABI is compatible + * with @ref QDMI_CLIENT_ABI_VERSION if and only if both versions have the same + * major and minor fields. A loader must ignore the patch field when it decides + * compatibility. A different major or minor field is incompatible. Device + * library versions remain independent of this version. + * @return The packed Client Interface ABI version. + */ +QDMI_DRIVER_EXPORT uint32_t QDMI_driver_get_client_abi_version(void); + /** * @brief A handle for a device implementing the * @ref device_interface "QDMI Device Interface". * @details An opaque pointer to a type defined by the driver that encapsulates - * an implementation of the @ref device_interface "QDMI Device Interface". + * an implementation of the @ref device_interface "QDMI Device Interface". + * The handle belongs to the session that returned it and becomes invalid when + * that session is freed. */ typedef struct QDMI_Device_impl_d *QDMI_Device; @@ -67,6 +99,9 @@ typedef struct QDMI_Device_impl_d *QDMI_Device; * @brief Provides functions to manage sessions between the client and driver. * @details A session is a connection between a client and a QDMI driver that * allows the client to interact with the driver and the devices it manages. + * An initialized session exposes an immutable snapshot of the devices that + * the authenticated client can access. A later session can expose a different + * snapshot. * * The typical workflow for a client session is as follows: * - Allocate a session with @ref QDMI_session_alloc. @@ -95,8 +130,10 @@ typedef struct QDMI_Session_impl_d *QDMI_Session; * @ref client_session_interface "client session interface" to refer to the * session. * @param[out] session A handle to the session that is allocated. Must not be - * @c NULL. The session must be freed by calling @ref QDMI_session_free - * when it is no longer used. + * @c NULL. The driver sets @p session to @c NULL before any operation that can + * fail. On failure, no session is allocated and the client can retry. The + * session must be freed by calling @ref QDMI_session_free when it is no longer + * used. * @return @ref QDMI_SUCCESS if the session was allocated successfully. * @return @ref QDMI_ERROR_INVALIDARGUMENT if @p session is @c NULL. * @return @ref QDMI_ERROR_OUTOFMEM if memory space ran out. @@ -104,7 +141,7 @@ typedef struct QDMI_Session_impl_d *QDMI_Session; * @see QDMI_session_set_parameter * QDMI_session_init */ -int QDMI_session_alloc(QDMI_Session *session); +QDMI_DRIVER_EXPORT int QDMI_session_alloc(QDMI_Session *session); /** * @brief Enum of the session parameters that can be set via @ref @@ -243,9 +280,10 @@ typedef enum QDMI_SESSION_PARAMETER_T QDMI_Session_Parameter; * @endcode * @endparblock */ -int QDMI_session_set_parameter(QDMI_Session session, - QDMI_Session_Parameter param, size_t size, - const void *value); +QDMI_DRIVER_EXPORT int QDMI_session_set_parameter(QDMI_Session session, + QDMI_Session_Parameter param, + size_t size, + const void *value); /** * @brief Initialize a session. @@ -267,7 +305,7 @@ int QDMI_session_set_parameter(QDMI_Session session, * @see QDMI_session_set_parameter * QDMI_session_query_session_property */ -int QDMI_session_init(QDMI_Session session); +QDMI_DRIVER_EXPORT int QDMI_session_init(QDMI_Session session); /** * @brief Enum of the session properties that can be queried via @ref @@ -363,18 +401,20 @@ typedef enum QDMI_SESSION_PROPERTY_T QDMI_Session_Property; * @attention May only be called after the session has been successfully * initialized with @ref QDMI_session_init. */ -int QDMI_session_query_session_property(QDMI_Session session, - QDMI_Session_Property prop, size_t size, - void *value, size_t *size_ret); +QDMI_DRIVER_EXPORT int +QDMI_session_query_session_property(QDMI_Session session, + QDMI_Session_Property prop, size_t size, + void *value, size_t *size_ret); /** * @brief Free a session. - * @details This function frees the memory allocated for the session. - * Accessing a (dangling) handle to a device that was attached to the session - * after the session was freed is undefined behavior. + * @details This function frees the memory allocated for the session. The client + * must free all jobs created or retrieved through the session before this call. + * This call invalidates every device, site, and operation handle obtained from + * the session. Accessing any invalidated handle is undefined behavior. * @param[in] session The session to free. */ -void QDMI_session_free(QDMI_Session session); +QDMI_DRIVER_EXPORT void QDMI_session_free(QDMI_Session session); /** @} */ // end of client_session_interface @@ -434,9 +474,9 @@ void QDMI_session_free(QDMI_Session session); * @endcode * @endparblock */ -int QDMI_device_query_device_property(QDMI_Device device, - QDMI_Device_Property prop, size_t size, - void *value, size_t *size_ret); +QDMI_DRIVER_EXPORT int +QDMI_device_query_device_property(QDMI_Device device, QDMI_Device_Property prop, + size_t size, void *value, size_t *size_ret); /** * @brief Query a site property. @@ -489,9 +529,11 @@ int QDMI_device_query_device_property(QDMI_Device device, * @remark @ref QDMI_Site handles may be queried via @ref * QDMI_device_query_device_property with @ref QDMI_DEVICE_PROPERTY_SITES. */ -int QDMI_device_query_site_property(QDMI_Device device, QDMI_Site site, - QDMI_Site_Property prop, size_t size, - void *value, size_t *size_ret); +QDMI_DRIVER_EXPORT int QDMI_device_query_site_property(QDMI_Device device, + QDMI_Site site, + QDMI_Site_Property prop, + size_t size, void *value, + size_t *size_ret); /** * @brief Query an operation property. @@ -575,7 +617,7 @@ int QDMI_device_query_site_property(QDMI_Device device, QDMI_Site site, * QDMI_OPERATION_PROPERTY_QUBITSNUM and @ref * QDMI_OPERATION_PROPERTY_PARAMETERSNUM, respectively. */ -int QDMI_device_query_operation_property( +QDMI_DRIVER_EXPORT int QDMI_device_query_operation_property( QDMI_Device device, QDMI_Operation operation, size_t num_sites, const QDMI_Site *sites, size_t num_params, const double *params, QDMI_Operation_Property prop, size_t size, void *value, size_t *size_ret); @@ -607,7 +649,9 @@ int QDMI_device_query_operation_property( /** * @brief A handle for a client-side job. * @details An opaque pointer to a type defined by the driver that encapsulates - * all information about a job submitted to a device by a client. + * all information about a job submitted to a device by a client. The job + * belongs to the session that supplied its device handle. The client must free + * the job before it frees that session. * @remark Implementations of the underlying type will want to store the device * handle used to create the job in the job handle to be able to access the * device when needed. @@ -631,7 +675,8 @@ typedef struct QDMI_Job_impl_d *QDMI_Job; * current session. * @return @ref QDMI_ERROR_FATAL if job creation failed due to a fatal error. */ -int QDMI_device_create_job(QDMI_Device device, QDMI_Job *job); +QDMI_DRIVER_EXPORT int QDMI_device_create_job(QDMI_Device device, + QDMI_Job *job); /** * @brief Retrieve an existing job by its ID. @@ -664,8 +709,9 @@ int QDMI_device_create_job(QDMI_Device device, QDMI_Job *job); * @return @ref QDMI_ERROR_FATAL if retrieving the job failed due to a fatal * error. */ -int QDMI_session_retrieve_job_by_id(QDMI_Device device, const char *job_id, - QDMI_Job *job); +QDMI_DRIVER_EXPORT int QDMI_session_retrieve_job_by_id(QDMI_Device device, + const char *job_id, + QDMI_Job *job); /** * @brief Enum of the job parameters that can be set. @@ -775,8 +821,9 @@ typedef enum QDMI_JOB_PARAMETER_T QDMI_Job_Parameter; * @endcode * @endparblock */ -int QDMI_job_set_parameter(QDMI_Job job, QDMI_Job_Parameter param, size_t size, - const void *value); +QDMI_DRIVER_EXPORT int QDMI_job_set_parameter(QDMI_Job job, + QDMI_Job_Parameter param, + size_t size, const void *value); /** * @brief Enum of the job properties that can be queried via @ref @@ -908,8 +955,10 @@ typedef enum QDMI_JOB_PROPERTY_T QDMI_Job_Property; * @endcode * @endparblock */ -int QDMI_job_query_property(QDMI_Job job, QDMI_Job_Property prop, size_t size, - void *value, size_t *size_ret); +QDMI_DRIVER_EXPORT int QDMI_job_query_property(QDMI_Job job, + QDMI_Job_Property prop, + size_t size, void *value, + size_t *size_ret); /** * @brief Submit a job to the device. @@ -926,7 +975,7 @@ int QDMI_job_query_property(QDMI_Job job, QDMI_Job_Property prop, size_t size, * current session. * @return @ref QDMI_ERROR_FATAL if the job submission failed. */ -int QDMI_job_submit(QDMI_Job job); +QDMI_DRIVER_EXPORT int QDMI_job_submit(QDMI_Job job); /** * @brief Cancel an already submitted job. @@ -941,7 +990,7 @@ int QDMI_job_submit(QDMI_Job job); * current session. * @return @ref QDMI_ERROR_FATAL if the job could not be canceled. */ -int QDMI_job_cancel(QDMI_Job job); +QDMI_DRIVER_EXPORT int QDMI_job_cancel(QDMI_Job job); /** * @brief Check the status of a job. @@ -957,7 +1006,7 @@ int QDMI_job_cancel(QDMI_Job job); * current session. * @return @ref QDMI_ERROR_FATAL if the job status could not be checked. */ -int QDMI_job_check(QDMI_Job job, QDMI_Job_Status *status); +QDMI_DRIVER_EXPORT int QDMI_job_check(QDMI_Job job, QDMI_Job_Status *status); /** * @brief Wait for a job to finish. @@ -978,7 +1027,7 @@ int QDMI_job_check(QDMI_Job job, QDMI_Job_Status *status); * @return @ref QDMI_ERROR_FATAL if the job could not be waited for and this * function returns before the job has finished or has been canceled. */ -int QDMI_job_wait(QDMI_Job job, size_t timeout); +QDMI_DRIVER_EXPORT int QDMI_job_wait(QDMI_Job job, size_t timeout); /** * @brief Retrieve the results of a job. @@ -1030,8 +1079,9 @@ int QDMI_job_wait(QDMI_Job job, size_t timeout); * @endcode * @endparblock */ -int QDMI_job_get_results(QDMI_Job job, QDMI_Job_Result result, size_t size, - void *data, size_t *size_ret); +QDMI_DRIVER_EXPORT int QDMI_job_get_results(QDMI_Job job, + QDMI_Job_Result result, size_t size, + void *data, size_t *size_ret); /** * @brief Free a job. @@ -1041,7 +1091,7 @@ int QDMI_job_get_results(QDMI_Job job, QDMI_Job_Result result, size_t size, * device-specific. * @param[in] job The job to free. */ -void QDMI_job_free(QDMI_Job job); +QDMI_DRIVER_EXPORT void QDMI_job_free(QDMI_Job job); /** @} */ // end of client_job_interface diff --git a/include/qdmi/constants.h b/include/qdmi/constants.h index 345727d1..93702fb7 100644 --- a/include/qdmi/constants.h +++ b/include/qdmi/constants.h @@ -25,6 +25,21 @@ #ifndef QDMI_CONSTANTS_H #define QDMI_CONSTANTS_H +#include + +/// Pack a Semantic Versioning release into 32 bits. +/// Major and minor must fit in 10 bits each, and patch must fit in 12 bits. +#define QDMI_MAKE_VERSION(major, minor, patch) \ + ((((uint32_t)(major) & 0x3FFU) << 22U) | \ + (((uint32_t)(minor) & 0x3FFU) << 12U) | ((uint32_t)(patch) & 0xFFFU)) + +/// Extract the major component of a packed version. +#define QDMI_VERSION_MAJOR(version) (((uint32_t)(version) >> 22U) & 0x3FFU) +/// Extract the minor component of a packed version. +#define QDMI_VERSION_MINOR(version) (((uint32_t)(version) >> 12U) & 0x3FFU) +/// Extract the patch component of a packed version. +#define QDMI_VERSION_PATCH(version) ((uint32_t)(version) & 0xFFFU) + #ifdef __cplusplus extern "C" { #endif @@ -459,6 +474,26 @@ enum QDMI_DEVICE_PROPERTY_T { * cannot obtain a trustworthy queue length. */ QDMI_DEVICE_PROPERTY_QUEUELENGTH = 17, + /** + * @brief `char*` (string) The stable client-visible device identifier. + * @details The Client Interface must return a nonempty, NUL-terminated, + * opaque ID. IDs are unique within one initialized session and immutable for + * the lifetime of the corresponding @ref QDMI_Device handle. Equivalent + * sessions return the same ID across process restarts while the same logical + * resource exists. Clients persist the pair of driver deployment and device + * ID because IDs from different drivers need not be unique. + * + * The ID identifies the logical resource. It is not a display name, endpoint, + * pointer value, credential, library version, symbol prefix, or build-system + * target ID. A driver can namespace or replace an identifier reported by its + * underlying device implementation. + * + * This property is mandatory through @ref QDMI_device_query_device_property. + * A device implementation can return @ref QDMI_ERROR_NOTSUPPORTED through + * @ref QDMI_device_session_query_device_property; the driver must then supply + * the client-visible ID. + */ + QDMI_DEVICE_PROPERTY_ID = 18, /** * @brief The maximum value of the enum. * @details It can be used by devices for bounds checking and validation of @@ -467,7 +502,7 @@ enum QDMI_DEVICE_PROPERTY_T { * @attention This value must remain the last regular member of the enum * besides the custom members and must be updated when new members are added. */ - QDMI_DEVICE_PROPERTY_MAX = 18, + QDMI_DEVICE_PROPERTY_MAX = 19, /** * @brief This enum value is reserved for a custom property. * @details The device defines the meaning and the type of this property. diff --git a/include/qdmi/export.h b/include/qdmi/export.h index 1ab6efe1..5cd1c0e0 100644 --- a/include/qdmi/export.h +++ b/include/qdmi/export.h @@ -27,6 +27,8 @@ /// Mark symbols as visible to other modules (no effect on static library /// builds). #define QDMI_EXPORT +/// Export Client Interface symbols from a static driver (no effect). +#define QDMI_DRIVER_EXPORT /// Mark symbols as not visible to other modules (no effect on static library /// builds). #define QDMI_NO_EXPORT @@ -53,6 +55,26 @@ #endif #endif +#ifndef QDMI_DRIVER_EXPORT +#ifdef QDMI_driver_EXPORTS +#ifdef _WIN32 +/// Export a QDMI Client Interface symbol from a driver library. +#define QDMI_DRIVER_EXPORT __declspec(dllexport) +#else +/// Export a QDMI Client Interface symbol from a driver library. +#define QDMI_DRIVER_EXPORT __attribute__((visibility("default"))) +#endif +#else +#ifdef _WIN32 +/// Import a QDMI Client Interface symbol from a driver library. +#define QDMI_DRIVER_EXPORT __declspec(dllimport) +#else +/// Import a QDMI Client Interface symbol from a driver library. +#define QDMI_DRIVER_EXPORT __attribute__((visibility("default"))) +#endif +#endif +#endif + #ifndef QDMI_NO_EXPORT #ifdef _WIN32 /// Mark symbols as not visible to other modules. diff --git a/templates/device/CMakeLists.txt b/templates/device/CMakeLists.txt index aeadca8f..95ed1015 100644 --- a/templates/device/CMakeLists.txt +++ b/templates/device/CMakeLists.txt @@ -53,7 +53,7 @@ set(CMAKE_VERIFY_INTERFACE_HEADER_SETS set(QDMI_PREFIX "MY") set(MY_QDMI_DEVICE_ID "my.default" - CACHE STRING "Stable identifier for the MY QDMI Device") + CACHE STRING "Package identifier for the MY QDMI Device") cmake_dependent_option( INSTALL_MY_QDMI_DEVICE @@ -86,7 +86,7 @@ if(BUILD_MY_QDMI_TESTS) get_target_property(EXPORTED_QDMI_DEVICE_PREFIX ${QDMI_TARGET_NAME} QDMI_DEVICE_PREFIX) if(NOT EXPORTED_QDMI_DEVICE_ID STREQUAL "${MY_QDMI_DEVICE_ID}") - message(FATAL_ERROR "The MY QDMI target does not export its stable ID") + message(FATAL_ERROR "The MY QDMI target does not export its package ID") endif() if(NOT EXPORTED_QDMI_DEVICE_PREFIX STREQUAL "${QDMI_PREFIX}") message(FATAL_ERROR "The MY QDMI target does not export its symbol prefix") diff --git a/templates/device/README.md b/templates/device/README.md index 2b115cbe..eecd8ba0 100644 --- a/templates/device/README.md +++ b/templates/device/README.md @@ -15,11 +15,11 @@ A C++20 library that implements the QDMI Device interface. -The exported CMake target publishes the stable device ID configured through +The exported CMake target publishes the package ID configured through `MY_QDMI_DEVICE_ID` and the QDMI symbol prefix through `configure_qdmi_device_target`. Consumers such as MQT Core can use this metadata -to package and register the device without project-specific loader code. This -metadata does not add MQT Core as a dependency. +to package and register the device without project-specific loader code. The +package ID is not the runtime `QDMI_DEVICE_PROPERTY_ID`. Do not allow C++ exceptions to escape the device's C entry points. Contain them directly at entry points that call throwing code, for example with a diff --git a/test/test_qdmi.cpp b/test/test_qdmi.cpp index 3bedab9b..d12ae011 100644 --- a/test/test_qdmi.cpp +++ b/test/test_qdmi.cpp @@ -20,7 +20,6 @@ #include "example_fomac.hpp" #include "example_tool.hpp" #include "qdmi/client.h" -#include "qdmi_example_driver.h" #include "utils/test_impl.hpp" #include @@ -29,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -47,6 +47,10 @@ extern "C" { #include namespace { +static_assert(QDMI_VERSION_MAJOR(QDMI_MAKE_VERSION(2, 1, 3)) == 2U); +static_assert(QDMI_VERSION_MINOR(QDMI_MAKE_VERSION(2, 1, 3)) == 1U); +static_assert(QDMI_VERSION_PATCH(QDMI_MAKE_VERSION(2, 1, 3)) == 3U); + /// Hash function for a pair struct Pair_hash { template @@ -57,6 +61,122 @@ struct Pair_hash { }; } // namespace +TEST(QDMIDriverLoadingTest, LazyInitializationIsTransactionalAndRetryable) { + EXPECT_EQ(QDMI_driver_get_client_abi_version(), QDMI_CLIENT_ABI_VERSION); + const char *original_conf = std::getenv("QDMI_CONF"); + const std::string saved_conf = original_conf != nullptr ? original_conf : ""; + const char *original_home = std::getenv("HOME"); + const std::string saved_home = original_home != nullptr ? original_home : ""; + + QDMI_Session session = nullptr; +#ifdef _WIN32 + _putenv_s("QDMI_CONF", "/nonexistent/path/to/qdmi.conf"); +#else + setenv("QDMI_CONF", "/nonexistent/path/to/qdmi.conf", 1); +#endif + EXPECT_NE(QDMI_session_alloc(&session), QDMI_SUCCESS); + EXPECT_EQ(session, nullptr); + + const std::string invalid_library_config = "qdmi_invalid_library.conf"; + { + std::ofstream config(invalid_library_config); + config << "/nonexistent/path/to/library" << Shared_library_file_extension() + << " CXX missing.device\n"; + } +#ifdef _WIN32 + _putenv_s("QDMI_CONF", invalid_library_config.c_str()); +#else + setenv("QDMI_CONF", invalid_library_config.c_str(), 1); +#endif + EXPECT_NE(QDMI_session_alloc(&session), QDMI_SUCCESS); + EXPECT_EQ(session, nullptr); + + const std::string duplicate_id_config = "qdmi_duplicate_id.conf"; + { + std::ofstream config(duplicate_id_config); + const std::string library = "../examples/device/src/libcxx-qdmi-device" + + std::string(Shared_library_file_extension()); + config << library << " CXX duplicate.device\n" + << library << " CXX duplicate.device\n"; + } +#ifdef _WIN32 + _putenv_s("QDMI_CONF", duplicate_id_config.c_str()); +#else + setenv("QDMI_CONF", duplicate_id_config.c_str(), 1); +#endif + EXPECT_NE(QDMI_session_alloc(&session), QDMI_SUCCESS); + EXPECT_EQ(session, nullptr); + + const std::string duplicate_library_config = "qdmi_duplicate_library.conf"; + { + std::ofstream config(duplicate_library_config); + const std::string library = "../examples/device/src/libcxx-qdmi-device" + + std::string(Shared_library_file_extension()); + config << library << " CXX first.device\n" + << library << " CXX second.device\n"; + } +#ifdef _WIN32 + _putenv_s("QDMI_CONF", duplicate_library_config.c_str()); +#else + setenv("QDMI_CONF", duplicate_library_config.c_str(), 1); +#endif + EXPECT_NE(QDMI_session_alloc(&session), QDMI_SUCCESS); + EXPECT_EQ(session, nullptr); + + const std::string failing_initialize_config = "qdmi_failing_initialize.conf"; + { + std::ofstream config(failing_initialize_config); + config << "../templates/device/src/libmy-qdmi-device" + << Shared_library_file_extension() << " MY template.device\n"; + } +#ifdef _WIN32 + _putenv_s("QDMI_CONF", failing_initialize_config.c_str()); +#else + setenv("QDMI_CONF", failing_initialize_config.c_str(), 1); +#endif + EXPECT_NE(QDMI_session_alloc(&session), QDMI_SUCCESS); + EXPECT_EQ(session, nullptr); + + const std::string default_config = "qdmi.conf"; + { + std::ofstream config(default_config); + config << "\n# Ignore comments and empty lines.\n" + << "../examples/device/src/libcxx-qdmi-device" + << Shared_library_file_extension() << " CXX example.cxx-simulator\n"; + } +#ifdef _WIN32 + _putenv_s("QDMI_CONF", ""); + _putenv_s("HOME", "/nonexistent/home/directory"); +#else + unsetenv("QDMI_CONF"); + setenv("HOME", "/nonexistent/home/directory", 1); +#endif + EXPECT_EQ(QDMI_session_alloc(&session), QDMI_SUCCESS); + EXPECT_NE(session, nullptr); + QDMI_session_free(session); + + std::filesystem::remove(invalid_library_config); + std::filesystem::remove(duplicate_id_config); + std::filesystem::remove(duplicate_library_config); + std::filesystem::remove(failing_initialize_config); + std::filesystem::remove(default_config); +#ifdef _WIN32 + _putenv_s("QDMI_CONF", saved_conf.c_str()); + _putenv_s("HOME", saved_home.c_str()); +#else + if (saved_conf.empty()) { + unsetenv("QDMI_CONF"); + } else { + setenv("QDMI_CONF", saved_conf.c_str(), 1); + } + if (saved_home.empty()) { + unsetenv("HOME"); + } else { + setenv("HOME", saved_home.c_str(), 1); + } +#endif +} + // Instantiate the test suite with different parameters INSTANTIATE_TEST_SUITE_P( QDMIDevice, @@ -65,11 +185,13 @@ INSTANTIATE_TEST_SUITE_P( // Test suite name // Parameters to test with ::testing::Values(std::tuple{"../examples/device/src/libcxx-qdmi-device", - "CXX", TEST_SESSION_MODE::READONLY}, + "CXX", "example.cxx-simulator", + TEST_SESSION_MODE::READONLY}, std::tuple{"../examples/device/src/libcxx-qdmi-device", - "CXX", TEST_SESSION_MODE::READWRITE}), - [](const testing::TestParamInfo< - std::tuple> &inf) { + "CXX", "example.cxx-simulator", + TEST_SESSION_MODE::READWRITE}), + [](const testing::TestParamInfo> &inf) { // Extract the last part of the file path const size_t pos = std::get<0>(inf.param).find_last_of("/\\"); std::string filename = (pos == std::string::npos) @@ -86,7 +208,7 @@ INSTANTIATE_TEST_SUITE_P( } // return name for the test - switch (std::get<2>(inf.param)) { + switch (std::get<3>(inf.param)) { case TEST_SESSION_MODE::READONLY: return filename + "__readonly"; case TEST_SESSION_MODE::READWRITE: @@ -1067,6 +1189,7 @@ TEST_P(QDMIImplementationTest, SessionInit) { test_token.c_str()), QDMI_SUCCESS); EXPECT_EQ(QDMI_session_init(session2), QDMI_SUCCESS); + QDMI_session_free(session2); } TEST_P(QDMIImplementationTest, RetrieveJobById) { @@ -1140,6 +1263,47 @@ TEST_P(QDMIImplementationTest, SessionQuerySessionProperty) { session, QDMI_SESSION_PROPERTY_DEVICES, devices_size, static_cast(devices_vec.data()), nullptr), QDMI_SUCCESS); + QDMI_session_free(session2); +} + +TEST_P(QDMIImplementationTest, ClientVisibleDeviceIdIsStable) { + size_t id_size = 0; + ASSERT_EQ(QDMI_device_query_device_property(device, QDMI_DEVICE_PROPERTY_ID, + 0, nullptr, &id_size), + QDMI_SUCCESS); + ASSERT_GT(id_size, 1U); + std::vector id(id_size); + ASSERT_EQ(QDMI_device_query_device_property(device, QDMI_DEVICE_PROPERTY_ID, + id.size(), id.data(), nullptr), + QDMI_SUCCESS); + EXPECT_EQ(id.back(), '\0'); + EXPECT_EQ(std::string(id.data()), "example.cxx-simulator"); + EXPECT_EQ(QDMI_device_query_device_property(device, QDMI_DEVICE_PROPERTY_ID, + id.size() - 1, id.data(), + nullptr), + QDMI_ERROR_INVALIDARGUMENT); + + QDMI_Session second_session = nullptr; + ASSERT_EQ(QDMI_session_alloc(&second_session), QDMI_SUCCESS); + const char *token = mode == TEST_SESSION_MODE::READWRITE ? "token" : ""; + ASSERT_EQ(QDMI_session_set_parameter(second_session, + QDMI_SESSION_PARAMETER_TOKEN, + std::strlen(token) + 1, token), + QDMI_SUCCESS); + ASSERT_EQ(QDMI_session_init(second_session), QDMI_SUCCESS); + QDMI_Device second_device = nullptr; + ASSERT_EQ(QDMI_session_query_session_property( + second_session, QDMI_SESSION_PROPERTY_DEVICES, + sizeof(QDMI_Device), static_cast(&second_device), + nullptr), + QDMI_SUCCESS); + std::vector second_id(id_size); + EXPECT_EQ(QDMI_device_query_device_property( + second_device, QDMI_DEVICE_PROPERTY_ID, second_id.size(), + second_id.data(), nullptr), + QDMI_SUCCESS); + EXPECT_EQ(second_id, id); + QDMI_session_free(second_session); } TEST_P(QDMIImplementationTest, SupportsCalibration) { @@ -1173,137 +1337,3 @@ TEST_P(QDMIImplementationTest, QueryPulseSupportLevel) { EXPECT_EQ(ret, QDMI_SUCCESS); EXPECT_EQ(pulse_support_level, QDMI_DEVICE_PULSE_SUPPORT_LEVEL_NONE); } - -// Standalone tests for driver library loading corner cases -TEST(QDMIDriverLoadingTest, LoadConfigWithNonExistentFile) { - // Save the original QDMI_CONF environment variable - const char *original_conf = std::getenv("QDMI_CONF"); - const std::string saved_conf = - (original_conf != nullptr) ? original_conf : ""; - -#ifdef _WIN32 - _putenv_s("QDMI_CONF", "/nonexistent/path/to/qdmi.conf"); -#else - setenv("QDMI_CONF", "/nonexistent/path/to/qdmi.conf", 1); -#endif - - // Driver initialization should fail because the config file doesn't exist - const auto init_result = QDMI_driver_init(); - - // Clean up before assertions - QDMI_driver_shutdown(); - - // Restore the original QDMI_CONF environment variable -#ifdef _WIN32 - _putenv_s("QDMI_CONF", saved_conf.c_str()); -#else - setenv("QDMI_CONF", saved_conf.c_str(), 1); -#endif - - // Now perform the assertion after cleanup - EXPECT_NE(init_result, QDMI_SUCCESS) - << "Driver should fail to initialize with non-existent config file"; -} - -TEST(QDMIDriverLoadingTest, LoadLibraryWithNonExistentPath) { - // Save the original QDMI_CONF environment variable - const char *original_conf = std::getenv("QDMI_CONF"); - const std::string saved_conf = - (original_conf != nullptr) ? original_conf : ""; - - // Create a config file pointing to a non-existent library path - const std::string config_file_name = "qdmi_nonexistent_library.conf"; - std::ofstream conf_file(config_file_name); - conf_file << "/nonexistent/path/to/library" << Shared_library_file_extension() - << " CXX\n"; - conf_file.close(); - -#ifdef _WIN32 - _putenv_s("QDMI_CONF", config_file_name.c_str()); -#else - setenv("QDMI_CONF", config_file_name.c_str(), 1); -#endif - - // Driver initialization should fail because the library path doesn't exist - // The Is_path_allowed function should return false when the path cannot be - // canonicalized - const auto init_result = QDMI_driver_init(); - - // Clean up before assertions - QDMI_driver_shutdown(); - std::filesystem::remove(config_file_name); - - // Restore the original QDMI_CONF environment variable -#ifdef _WIN32 - if (!saved_conf.empty()) { - _putenv_s("QDMI_CONF", saved_conf.c_str()); - } else { - _putenv_s("QDMI_CONF", ""); - } -#else - if (!saved_conf.empty()) { - setenv("QDMI_CONF", saved_conf.c_str(), 1); - } else { - unsetenv("QDMI_CONF"); - } -#endif - - // Now perform the assertion after cleanup - EXPECT_NE(init_result, QDMI_SUCCESS) - << "Driver should fail to initialize with non-existent library path"; -} - -TEST(QDMIDriverLoadingTest, LoadLibraryWithInvalidHomeEnv) { - // Save the original HOME environment variable - const char *original_home = std::getenv("HOME"); - const std::string saved_home = - (original_home != nullptr) ? original_home : ""; - - // Set HOME to a non-existent path -#ifdef _WIN32 - _putenv_s("HOME", "/nonexistent/home/directory"); -#else - setenv("HOME", "/nonexistent/home/directory", 1); -#endif - - // Create a valid config file pointing to the example device in the current - // directory (test runs from build directory, library is in examples/device/) - const std::string config_file_name = "qdmi_invalid_home.conf"; - std::ofstream conf_file(config_file_name); - conf_file << "../examples/device/src/libcxx-qdmi-device" - << Shared_library_file_extension() << " CXX\n"; - conf_file.close(); - -#ifdef _WIN32 - _putenv_s("QDMI_CONF", config_file_name.c_str()); -#else - setenv("QDMI_CONF", config_file_name.c_str(), 1); -#endif - - // Driver initialization should succeed because the library is in an allowed - // path (current directory) even though HOME is invalid. The Is_path_allowed - // function should catch the exception when canonicalizing HOME and skip it. - const auto init_result = QDMI_driver_init(); - - // Clean up and restore the environment BEFORE any assertions - QDMI_driver_shutdown(); - std::filesystem::remove(config_file_name); - - // Restore the original HOME environment variable -#ifdef _WIN32 - if (!saved_home.empty()) { - _putenv_s("HOME", saved_home.c_str()); - } -#else - if (!saved_home.empty()) { - setenv("HOME", saved_home.c_str(), 1); - } else { - unsetenv("HOME"); - } -#endif - - // Now perform the assertion after cleanup - EXPECT_EQ(init_result, QDMI_SUCCESS) - << "Driver should initialize successfully even with invalid HOME " - "environment variable"; -} diff --git a/test/utils/test_impl.cpp b/test/utils/test_impl.cpp index 2e594040..90e8f5c6 100644 --- a/test/utils/test_impl.cpp +++ b/test/utils/test_impl.cpp @@ -20,7 +20,6 @@ #include "test_impl.hpp" #include "qdmi/client.h" -#include "qdmi_example_driver.h" #include #include @@ -37,7 +36,8 @@ void QDMIImplementationTest::SetUp() { auto params = GetParam(); const std::string &library_name = std::get<0>(params); const std::string &prefix = std::get<1>(params); - mode = std::get<2>(params); + const std::string &device_id = std::get<2>(params); + mode = std::get<3>(params); // Get the current test info const ::testing::TestInfo *test_info = @@ -50,7 +50,7 @@ void QDMIImplementationTest::SetUp() { config_file_name = "qdmi_" + test_name + ".conf"; std::ofstream conf_file(config_file_name); conf_file << library_name << Shared_library_file_extension() << " " << prefix - << "\n"; + << " " << device_id << "\n"; conf_file.close(); #ifdef _WIN32 @@ -60,9 +60,6 @@ void QDMIImplementationTest::SetUp() { setenv("QDMI_CONF", config_file_name.c_str(), 1); #endif - ASSERT_EQ(QDMI_driver_init(), QDMI_SUCCESS) - << "Failed to initialize the driver"; - ASSERT_EQ(QDMI_session_alloc(&session), QDMI_SUCCESS) << "Failed to allocate session"; @@ -97,7 +94,6 @@ void QDMIImplementationTest::SetUp() { void QDMIImplementationTest::TearDown() { QDMI_session_free(session); - QDMI_driver_shutdown(); std::filesystem::remove(config_file_name); } @@ -121,6 +117,7 @@ TEST_P(QDMIImplementationTest, SessionSetParameterImplemented) { "https://example.com"), testing::AnyOf(QDMI_SUCCESS, QDMI_ERROR_NOTSUPPORTED, QDMI_ERROR_INVALIDARGUMENT)); + QDMI_session_free(uninitialized_session); EXPECT_EQ(QDMI_session_set_parameter(session, QDMI_SESSION_PARAMETER_AUTHURL, 20, "https://example.com"), QDMI_ERROR_BADSTATE); diff --git a/test/utils/test_impl.hpp b/test/utils/test_impl.hpp index c84bd610..9cfa7eaa 100644 --- a/test/utils/test_impl.hpp +++ b/test/utils/test_impl.hpp @@ -49,8 +49,8 @@ constexpr const char *Shared_library_file_extension() { enum class TEST_SESSION_MODE : uint8_t { READONLY, READWRITE }; class QDMIImplementationTest - : public ::testing::TestWithParam< - std::tuple> { + : public ::testing::TestWithParam> { protected: QDMI_Session session = nullptr; QDMI_Device device = nullptr;