diff --git a/MMCore/CoreFeatures.cpp b/MMCore/CoreFeatures.cpp new file mode 100644 index 000000000..f05beb0ab --- /dev/null +++ b/MMCore/CoreFeatures.cpp @@ -0,0 +1,140 @@ +// PROJECT: Micro-Manager +// SUBSYSTEM: MMCore +// +// COPYRIGHT: 2023, Board of Regents of the University of Wisconsin System +// All Rights reserved +// +// LICENSE: This file is distributed under the "Lesser GPL" (LGPL) license. +// License text is included with the source distribution. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// +// IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. +// +// AUTHOR: Mark Tsuchida + +#include "CoreFeatures.h" + +#include "Error.h" + +#include +#include +#include + +// Core Features (inspired by chrome://flags) +// +// Core features are named boolean flags that control API behavior. They can be +// used for these purposes: +// - Providing a migration path for API changes, especially related to error +// handling (stricter checks or reporting previously ignored errors). +// - Providing a way to develop new features (especially complex ones) without +// forking or branching MMCore. The new feature can be disabled by default +// until its API is well-vetted and stable, preventing casual users from +// accidentally using the experimental feature without realizing. Trying to +// access the new feature without enabling it would generally result in an +// exception. +// - Possibly other purposes, provided that care is taken to ensure that +// switching the feature does _not_ result in effectively two mutually +// incompatible variants of MMCore. +// +// Importantly, feature switches are a migration strategy, _not_ a permanent +// configuration mechanism. Every feature must have the property: one or the +// other setting of the feature allows old and new user code to run. (When used +// for the introduction of better error checking, disabling the feature should +// not break user code that is compatible with the new error checking. When +// used for the introduction of new functionality, enabling the feature should +// not break compatibility with existing user code.) +// +// Typically, switching features is done manually and locally (for testing new +// behavior), or is done once during initialization by the application/library +// providing the overall environment (such as MMStudio or its analogue). +// +// How to add a new feature: +// - Add a bool flag to struct mm::features::Flags (in the .h file), with its +// default value (usually false for a brand-new feature) +// - Add the feature name and getter/setter lambdas in the map inside +// featureMap() (below) +// - Document the feature in the Doxygen comment for CMMCore::enableFeature() +// (internal notes about the feature that are not useful to the user should +// be documented in featureMap()) +// - In Core code, query the feature state with: mm::features::flags().name +// +// Lifecycle of a feature: +// - Features should generally be disabled by default when first added. When +// the feature represents experimental functionality, breaking changes can be +// made to the new functionality while it remains in this stage. +// - When the feature is ready for widespread use, it should be enabled by +// default. If this causes a backward-incompatible change in default Core API +// behavior, this change requires MMCore's major version to be incremented; +// disabling the feature should usually be deprecated. If it causes new +// functions to be available by default, MMCore's minor version should be +// incremented; disabling the feature may be forbidden. +// - When the old behavior (i.e., feature disabled) is no longer needed, the +// feature should be permanently enabled: the getter should then always +// return true, the setter should throw an exception, and the corresponding +// flag in mm::features::Flags should be removed. However, the feature name +// should never be removed. +// - There may be cases where a feature is abandoned before becoming enabled by +// default. In this case, it should be permanently disabled. + +namespace mm { +namespace features { + +namespace internal { + +Flags g_flags{}; + +} + +namespace { + +const auto& featureMap() { + // Here we define the mapping from feature names to what they do. + // Use functions (lambdas) to get/set the flags, so that we have the + // possibility of having feature names that enable sets of features. + using GetFunc = bool(*)(); + using SetFunc = void(*)(bool); + using internal::g_flags; + static const std::map> map = { + { + "StrictInitializationChecks", { + [] { return g_flags.strictInitializationChecks; }, + [](bool e) { g_flags.strictInitializationChecks = e; } + // This is made switchable to give user code (mainly the MMStudio + // Hardware Configuration Wizard) time to be fixed and tested, + // while allowing other environments to benefit from safer + // behavior. It should be enabled by default when we no longer have + // prominent users that require the old behavior, and permanently + // enabled perhaps a few years later. + } + }, + // How to add a new Core feature: see the comment at the top of this file. + // Features (the string names) must never be removed once added! + }; + return map; +} + +} + +void enableFeature(const std::string& name, bool enable) { + try { + featureMap().at(name).second(enable); + } catch (const std::out_of_range&) { + throw CMMError("No such feature: " + name); + } +} + +bool isFeatureEnabled(const std::string& name) { + try { + return featureMap().at(name).first(); + } catch (const std::out_of_range&) { + throw CMMError("No such feature: " + name); + } +} + +} // namespace features +} // namespace mm diff --git a/MMCore/CoreFeatures.h b/MMCore/CoreFeatures.h new file mode 100644 index 000000000..3cd04b19d --- /dev/null +++ b/MMCore/CoreFeatures.h @@ -0,0 +1,44 @@ +// PROJECT: Micro-Manager +// SUBSYSTEM: MMCore +// +// COPYRIGHT: 2023, Board of Regents of the University of Wisconsin System +// All Rights reserved +// +// LICENSE: This file is distributed under the "Lesser GPL" (LGPL) license. +// License text is included with the source distribution. +// +// This file is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// +// IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES. +// +// AUTHOR: Mark Tsuchida + +#pragma once + +#include + +namespace mm { +namespace features { + +struct Flags { + bool strictInitializationChecks = false; + // How to add a new Core feature: see the comment in the .cpp file. +}; + +namespace internal { + +extern Flags g_flags; + +} + +inline const Flags& flags() { return internal::g_flags; } + +void enableFeature(const std::string& name, bool enable); +bool isFeatureEnabled(const std::string& name); + +} // namespace features +} // namespace mm diff --git a/MMCore/Devices/DeviceInstance.cpp b/MMCore/Devices/DeviceInstance.cpp index caa6d0e31..2b800d8cd 100644 --- a/MMCore/Devices/DeviceInstance.cpp +++ b/MMCore/Devices/DeviceInstance.cpp @@ -22,6 +22,7 @@ #include "DeviceInstance.h" #include "../../MMDevice/MMDevice.h" +#include "../CoreFeatures.h" #include "../CoreUtils.h" #include "../Error.h" #include "../LoadableModules/LoadedDeviceAdapter.h" @@ -122,16 +123,22 @@ DeviceInstance::ThrowIfError(int code, const std::string& message) const } void -DeviceInstance::RequireInitialized(const char *operation) const -{ - if (!initialized_) { - // This is an error, but existing application code (in particular, - // the Hardware Configuration Wizard) breaks if we enforce it strictly. - // Until such code is fixed, we only log. - LOG_WARNING(Logger()) << "Operation (" << operation << - ") not permitted on uninitialized device (this will be an error in a future version of MMCore; for now we continue with the operation anyway, even though it might not be safe)"; - // Eventually to be replaced with: - // ThrowError("Operation not permitted on uninitialized device"); +DeviceInstance::RequireInitialized(const char* operation) const +{ + if (!initialized_) + { + if (mm::features::flags().strictInitializationChecks) + { + std::ostringstream stream; + stream << "Operation (" << operation << + ") not permitted on uninitialized device"; + ThrowError(stream.str()); + } + else + { + LOG_WARNING(Logger()) << "Operation (" << operation << + ") not permitted on uninitialized device (this will be an error in a future version of MMCore; for now we continue with the operation anyway, even though it might not be safe)"; + } } } diff --git a/MMCore/MMCore.cpp b/MMCore/MMCore.cpp index a04aaa6ef..b458f92cf 100644 --- a/MMCore/MMCore.cpp +++ b/MMCore/MMCore.cpp @@ -46,6 +46,7 @@ #include "ConfigGroup.h" #include "Configuration.h" #include "CoreCallback.h" +#include "CoreFeatures.h" #include "CoreProperty.h" #include "CoreUtils.h" #include "DeviceManager.h" @@ -101,7 +102,7 @@ using namespace std; * (Keep the 3 numbers on one line to make it easier to look at diffs when * merging/rebasing.) */ -const int MMCore_versionMajor = 11, MMCore_versionMinor = 0, MMCore_versionPatch = 0; +const int MMCore_versionMajor = 11, MMCore_versionMinor = 1, MMCore_versionPatch = 0; /////////////////////////////////////////////////////////////////////////////// @@ -177,6 +178,56 @@ CMMCore::~CMMCore() LOG_INFO(coreLogger_) << "Core session ended"; } +/** + * Enable or disable the given Core feature. + * + * Core features control whether experimental functionality (which is subject + * to breaking changes) is exposed, or whether stricter API usage is enforced. + * + * Currently switchable features: + * - "StrictInitializationChecks" (default: disabled) When enabled, an + * exception is thrown when an operation requiring an initialized device is + * attempted on a device that is not successfully initialized. When disabled, + * no exception is thrown and a warning is logged (and the operation may + * potentially cause incorrect behavior or a crash). + * + * Permanently enabled features: + * - None so far. + * + * Permanently disabled features: + * - None so far. + * + * @param name the feature name. + * @param enable whether to enable or disable the feature. + * + * @throws CMMError if the feature name is null or unknown, or attempting to + * disable a permanently enabled feature, or attempting to enable a permanently + * disabled feature. + */ +void CMMCore::enableFeature(const char* name, bool enable) throw (CMMError) +{ + if (name == nullptr) + throw CMMError("Null feature name", MMERR_NullPointerException); + mm::features::enableFeature(name, enable); +} + +/** + * Return whether the given Core feature is currently enabled. + * + * See enableFeature() for the available features. + * + * @param name the feature name. + * @returns whether the feature is enabled. + * + * @throws CMMError if the feature name is null or unknown. + */ +bool CMMCore::isFeatureEnabled(const char* name) throw (CMMError) +{ + if (name == nullptr) + throw CMMError("Null feature name", MMERR_NullPointerException); + return mm::features::isFeatureEnabled(name); +} + /** * Set the primary Core log file. * diff --git a/MMCore/MMCore.h b/MMCore/MMCore.h index aa355a9b8..79b2af386 100644 --- a/MMCore/MMCore.h +++ b/MMCore/MMCore.h @@ -144,6 +144,12 @@ class CMMCore */ static void noop() {} + /** \name Core feature control. */ + ///@{ + static void enableFeature(const char* name, bool enable) throw (CMMError); + static bool isFeatureEnabled(const char* name) throw (CMMError); + ///@} + /** \name Initialization and setup. */ ///@{ void loadDevice(const char* label, const char* moduleName, diff --git a/MMCore/MMCore.vcxproj b/MMCore/MMCore.vcxproj index e639480d5..d5e4c2f28 100644 --- a/MMCore/MMCore.vcxproj +++ b/MMCore/MMCore.vcxproj @@ -80,6 +80,7 @@ + @@ -118,6 +119,7 @@ + diff --git a/MMCore/MMCore.vcxproj.filters b/MMCore/MMCore.vcxproj.filters index d3009c657..1920583b6 100644 --- a/MMCore/MMCore.vcxproj.filters +++ b/MMCore/MMCore.vcxproj.filters @@ -42,6 +42,9 @@ Source Files + + Source Files + Source Files @@ -152,6 +155,9 @@ Header Files + + Header Files + Header Files diff --git a/MMCore/Makefile.am b/MMCore/Makefile.am index 1a2975a6d..d56ad5273 100644 --- a/MMCore/Makefile.am +++ b/MMCore/Makefile.am @@ -15,6 +15,8 @@ libMMCore_la_SOURCES = \ Configuration.h \ CoreCallback.cpp \ CoreCallback.h \ + CoreFeatures.cpp \ + CoreFeatures.h \ CoreProperty.cpp \ CoreProperty.h \ CoreUtils.h \ diff --git a/MMCoreJ_wrap/pom.xml b/MMCoreJ_wrap/pom.xml index a7f089a36..38d1ba920 100644 --- a/MMCoreJ_wrap/pom.xml +++ b/MMCoreJ_wrap/pom.xml @@ -3,7 +3,7 @@ org.micro-manager.mmcorej MMCoreJ jar - 11.0.0 + 11.1.0 Micro-Manager Java Interface to MMCore Micro-Manager is open source software for control of automated/motorized microscopes. This specific packages provides the Java interface to the device abstractino layer (MMCore) that is written in C++ with a C-interface http://micro-manager.org