Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MMCore: Add enableFeature() and isFeatureEnabled() #399

Merged
merged 1 commit into from
Nov 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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