Skip to content

Commit

Permalink
MMCore: Add enableFeature() and isFeatureEnabled()
Browse files Browse the repository at this point in the history
Also add the feature "StrictInitializationChecks".
  • Loading branch information
marktsuchida committed Nov 3, 2023
1 parent bd4704f commit aa6d2b7
Show file tree
Hide file tree
Showing 9 changed files with 270 additions and 12 deletions.
140 changes: 140 additions & 0 deletions MMCore/CoreFeatures.cpp
Original file line number Diff line number Diff line change
@@ -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 <map>
#include <stdexcept>
#include <utility>

// 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<std::string, std::pair<GetFunc, SetFunc>> 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
44 changes: 44 additions & 0 deletions MMCore/CoreFeatures.h
Original file line number Diff line number Diff line change
@@ -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 <string>

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
27 changes: 17 additions & 10 deletions MMCore/Devices/DeviceInstance.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include "DeviceInstance.h"

#include "../../MMDevice/MMDevice.h"
#include "../CoreFeatures.h"
#include "../CoreUtils.h"
#include "../Error.h"
#include "../LoadableModules/LoadedDeviceAdapter.h"
Expand Down Expand Up @@ -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)";
}
}
}

Expand Down
53 changes: 52 additions & 1 deletion MMCore/MMCore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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;


///////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -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.
*
Expand Down
6 changes: 6 additions & 0 deletions MMCore/MMCore.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions MMCore/MMCore.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
<ClCompile Include="CircularBuffer.cpp" />
<ClCompile Include="Configuration.cpp" />
<ClCompile Include="CoreCallback.cpp" />
<ClCompile Include="CoreFeatures.cpp" />
<ClCompile Include="CoreProperty.cpp" />
<ClCompile Include="DeviceManager.cpp" />
<ClCompile Include="Devices\AutoFocusInstance.cpp" />
Expand Down Expand Up @@ -118,6 +119,7 @@
<ClInclude Include="ConfigGroup.h" />
<ClInclude Include="Configuration.h" />
<ClInclude Include="CoreCallback.h" />
<ClInclude Include="CoreFeatures.h" />
<ClInclude Include="CoreProperty.h" />
<ClInclude Include="CoreUtils.h" />
<ClInclude Include="DeviceManager.h" />
Expand Down
6 changes: 6 additions & 0 deletions MMCore/MMCore.vcxproj.filters
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
<ClCompile Include="CoreCallback.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="CoreFeatures.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="CoreProperty.cpp">
<Filter>Source Files</Filter>
</ClCompile>
Expand Down Expand Up @@ -152,6 +155,9 @@
<ClInclude Include="CoreCallback.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="CoreFeatures.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="CoreProperty.h">
<Filter>Header Files</Filter>
</ClInclude>
Expand Down
2 changes: 2 additions & 0 deletions MMCore/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ libMMCore_la_SOURCES = \
Configuration.h \
CoreCallback.cpp \
CoreCallback.h \
CoreFeatures.cpp \
CoreFeatures.h \
CoreProperty.cpp \
CoreProperty.h \
CoreUtils.h \
Expand Down
2 changes: 1 addition & 1 deletion MMCoreJ_wrap/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<groupId>org.micro-manager.mmcorej</groupId>
<artifactId>MMCoreJ</artifactId>
<packaging>jar</packaging>
<version>11.0.0</version>
<version>11.1.0</version>
<name>Micro-Manager Java Interface to MMCore</name>
<description>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</description>
<url>http://micro-manager.org</url>
Expand Down

0 comments on commit aa6d2b7

Please sign in to comment.