LUMIERA.clone/src/proc/control/command-setup.cpp
Ichthyostega aecef2a8f4 Commands: refactor integration into SessionCommandService (#1089)
It seems more adequate to push the somewhat intricate mechanics
for the "fall back" onto generic commands down into the implementation
level of CommandInstanceManager. The point is, we know the standard
usage situation is to rely on the instance manager, and thus we want
to avoid redundant table lookups, only to support the rare case of
fallback to global commands. The latter is currently used only from
unit-tests, but might in future also be used by scripts.

Due to thread safety considerations, I have refrained from handing
out a direct reference to the command token sitting in the registry,
even while not doing so incurs a small runtime penalty (accessing
the shared ref-count for creating a copy of the smart-handle).
This is the typical situation where you'd be tempted to sacrifice
sanity for the sake of an imaginary performance benefit, which
in fact is dwarfed by all the machinery of UI-Bus and argument
passing via GenNode.
2017-04-09 19:11:40 +02:00

264 lines
10 KiB
C++

/*
CommandSetup - Implementation of command registration and instance management
Copyright (C) Lumiera.org
2017, Hermann Vosseler <Ichthyostega@web.de>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of
the License, or (at your option) any later version.
This program 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. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
* *****************************************************/
/** @file command-setup.cpp
** Implementation details of instance management for command invocation by the GUI.
** This translation unit combines the implementation code of several facilities
** related to definition and instantiation of concrete command instances.
**
** @see command-setup.hpp
** @see command-instance-manager.hpp
** @see CommandInstanceManager_test
** @see command.hpp
** @see command-registry.hpp
**
*/
#include "lib/error.hpp"
#include "include/logging.h"
#include "include/lifecycle.h"
#include "proc/control/command.hpp"
#include "proc/control/command-setup.hpp"
#include "proc/control/command-instance-manager.hpp"
#include "proc/control/command-def.hpp"
#include "lib/symbol.hpp"
#include "lib/format-string.hpp"
#include "lib/util.hpp"
#include <tuple>
#include <utility>
using std::tuple;
using std::get;
using std::function;
using std::move;
using lib::Symbol;
using lumiera::LifecycleHook;
using lumiera::ON_GLOBAL_INIT;
using std::string;
using util::unConst;
using util::_Fmt;
namespace proc {
namespace control {
namespace error = lumiera::error;
namespace { // implementation details: storage for pending static command definitions...
using CmdDefEntry = std::tuple<Symbol, DefinitionClosure>;
std::deque<CmdDefEntry> pendingCmdDefinitions;
}//(End) implementation details
CommandSetup::~CommandSetup() { }
/** Start a command setup for defining a Proc-Layer command with the given cmdID
* @param cmdID the ID under with the new command will be registered
* @note after defining a static variable of type CommandSetup,
* a functor or lambda should be assigned, which then
* provides the actual setup of the CommandDef
*/
CommandSetup::CommandSetup(Symbol cmdID)
: cmdID_(cmdID)
{ }
/**
* @param definitionBlock anything assignable to `function<void(CommandDef&)>`
* @remarks this operation is intended for a very specific usage pattern, as established
* by the macro #COMMAND_DEFINITION. The purpose is to feed a given code block
* into the hidden queue for command definitions, from where it will be issued
* at the lifecycle event ON_BASIC_INIT (typically at start of application `main()`).
* On invocation, the code block is provided with an still unbound CommandDef object,
* which has been registered under the Command-ID as stored in this CommandSetup object.
* The assumption is that this _definition closure_ will care to define the command,
* state capturing and undo operations for the command definition in question. Thus,
* the result of invoking this closure will be to store a complete command prototype
* into the proc::control::CommandRegistry.
* @note this operation works by side-effect; the given argument is fed into a hidden static
* queue, but not stored within the object instance.
* @warning invoking this assignment _several times on the same CommandSetup object_ will likely
* lead to an invalid state, causing the Lumiera application to fail on start-up. The
* reason for this is the fact that CommandDef rejects duplicate command definitions.
* Moreover, please note that invoking this operation at any point _after_ the
* lifecycle event ON_BASIC_INIT will likely have no effect at all, since the
* given closure will then just sit in the static queue and never be invoked.
*/
CommandSetup&
CommandSetup::operator= (DefinitionClosure definitionBlock)
{
if (not definitionBlock)
throw error::Invalid ("unbound function/closure provided for CommandSetup"
, error::LUMIERA_ERROR_BOTTOM_VALUE);
pendingCmdDefinitions.emplace_front (cmdID_, move(definitionBlock));
return *this;
}
size_t
CommandSetup::pendingCnt()
{
return pendingCmdDefinitions.size();
}
void
CommandSetup::invokeDefinitionClosures()
{
while (not pendingCmdDefinitions.empty())
{
CmdDefEntry& entry = pendingCmdDefinitions.back();
Symbol& cmdID{get<Symbol>(entry)};
DefinitionClosure& buildDefinition{get<DefinitionClosure> (entry)};
TRACE (command, "defining Command(%s)...", cmdID.c());
CommandDef def(cmdID);
buildDefinition(def);
pendingCmdDefinitions.pop_back();
}
}
namespace { // automatically invoke static command definitions
LifecycleHook schedule_ (ON_GLOBAL_INIT, &CommandSetup::invokeDefinitionClosures);
}
// emit dtors of embedded objects here....
CommandInstanceManager::~CommandInstanceManager() { }
/** create a CommandInstanceManager and wire it with the given CommandDispatch implementation.
* Typically, this is done in SessionCommandService. The embedded hash table for pending
* command instances is sized in relation to the number of registered global Command definitions.
*/
CommandInstanceManager::CommandInstanceManager (CommandDispatch& dispatcher)
: dispatcher_{dispatcher}
, table_{2 * Command::definition_count()}
{ }
/** Create and thus "open" a new anonymous command instance.
* @param prototypeID the underlying Command definition to create a clone copy
* @param invocationID used to decorate the prototypeID to from an unique instanceID
* @throws error::Logic in case an instance is for this ID combination is already "open"
*/
Symbol
CommandInstanceManager::newInstance (Symbol prototypeID, string const& invocationID)
{
Symbol instanceID{prototypeID, invocationID};
Command& instance = table_[instanceID];
if (instance)
throw new error::Logic (_Fmt{"Attempt to create a new Command instance '%s', "
"while an instance for this invocationID %s "
"is currently open for parametrisation and "
"not yet dispatched for execution."}
% instanceID % invocationID
, LUMIERA_ERROR_DUPLICATE_COMMAND
);
// create new clone from the prototype
table_[instanceID] = move (Command::get(prototypeID).newInstance());
ENSURE (instance, "cloning of command prototype failed");
return instanceID;
}
/** access the currently "opened" instance with the given instanceID
* @param instanceID ID as returned from #newInstance, or a global commandID
* @return instance handle of handle of a global command as fallback
* @note when given a commandID, which is not known as (decorated) instanceID
* within our local registration table, just the globally registered
* Command instance is returned.
* @remark deliberately this function returns by-value. Returning a reference
* into the global command registry would be dangerous under concurrency.
* @throw error::Invalid when the given cmdID unknown both locally and globally.
* @throw error::Logic when accessing an instance that _was_ known but is
* currently no longer "open" (already dispatched command instance)
*/
Command
CommandInstanceManager::getInstance (Symbol instanceID)
{
auto entry = table_.find(instanceID);
if (entry == table_.end())
return Command::get(instanceID);
if (not entry->second)
throw error::Logic (_Fmt{"Command instance '%s' is not (yet/anymore) active"}
% instanceID
, error::LUMIERA_ERROR_LIFECYCLE);
return entry->second;
}
/** hand over the designated command instance to the dispatcher installed
* on construction. Either the given ID corresponds to a previously "opened"
* local instance (known only to this instance manager). In this case, the
* instance will really be _moved_ over into the dispatcher, which also means
* this instance is no longer "open" for parametrisation. In the other case
* we try to retrieve the given ID from the global CommandRegistry and
* dispatch it, _without removing_ it from the global registry of course.
*/
void
CommandInstanceManager::dispatch (Symbol instanceID)
{
auto entry = table_.find(instanceID);
Command fallback;
Command& instance{entry == table_.end()? fallback = Command::get(instanceID)
: entry->second
};
if (not instance)
throw error::Logic (_Fmt{"attempt to dispatch command instance '%s' "
"without creating a new instance from prototype beforehand"}
% instanceID
, error::LUMIERA_ERROR_LIFECYCLE);
if (not instance.canExec())
throw error::State (_Fmt{"attempt to dispatch command instance '%s' "
"without binding all arguments properly beforehand"}
% instanceID
, LUMIERA_ERROR_UNBOUND_ARGUMENTS);
REQUIRE (instance and instance.canExec());
dispatcher_.enqueue(move (instance));
ENSURE (not instance);
}
bool
CommandInstanceManager::contains (Symbol instanceID) const
{
return util::contains (table_, instanceID)
and unConst(this)->table_[instanceID].isValid();
}
}} // namespace proc::control