mirror of
https://github.com/QuantumLeaps/qpcpp.git
synced 2025-01-28 06:02:56 +08:00
538 lines
21 KiB
C++
538 lines
21 KiB
C++
/// @file
|
|
/// @brief QF/C++ time events and time management services
|
|
/// @ingroup qf
|
|
/// @cond
|
|
///***************************************************************************
|
|
/// Last updated for version 5.9.0
|
|
/// Last updated on 2017-05-08
|
|
///
|
|
/// Q u a n t u m L e a P s
|
|
/// ---------------------------
|
|
/// innovating embedded systems
|
|
///
|
|
/// Copyright (C) Quantum Leaps, LLC. All rights reserved.
|
|
///
|
|
/// This program is open source 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 3 of the License, or
|
|
/// (at your option) any later version.
|
|
///
|
|
/// Alternatively, this program may be distributed and modified under the
|
|
/// terms of Quantum Leaps commercial licenses, which expressly supersede
|
|
/// the GNU General Public License and are specifically designed for
|
|
/// licensees interested in retaining the proprietary status of their code.
|
|
///
|
|
/// 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, see <http://www.gnu.org/licenses/>.
|
|
///
|
|
/// Contact information:
|
|
/// http:www.state-machine.com
|
|
/// mailto:info@state-machine.com
|
|
///***************************************************************************
|
|
/// @endcond
|
|
|
|
#define QP_IMPL // this is QP implementation
|
|
#include "qf_port.h" // QF port
|
|
#include "qf_pkg.h" // QF package-scope interface
|
|
#include "qassert.h" // QP embedded systems-friendly assertions
|
|
#ifdef Q_SPY // QS software tracing enabled?
|
|
#include "qs_port.h" // include QS port
|
|
#else
|
|
#include "qs_dummy.h" // disable the QS software tracing
|
|
#endif // Q_SPY
|
|
|
|
|
|
namespace QP {
|
|
|
|
Q_DEFINE_THIS_MODULE("qf_time")
|
|
|
|
// Package-scope objects *****************************************************
|
|
QTimeEvt QF::timeEvtHead_[QF_MAX_TICK_RATE]; // heads of time event lists
|
|
|
|
//****************************************************************************
|
|
/// @description
|
|
/// This function must be called periodically from a time-tick ISR or from
|
|
/// a task so that QF can manage the timeout events assigned to the given
|
|
/// system clock tick rate.
|
|
///
|
|
/// @param[in] tickRate system clock tick rate serviced in this call.
|
|
///
|
|
/// @note this function should be called only via the macro TICK_X()
|
|
///
|
|
/// @note the calls to QP::QF::tickX_() with different tick rate argument can
|
|
/// preempt each other. For example, higher clock tick rates might be
|
|
/// serviced from interrupts while others from tasks (active objects).
|
|
///
|
|
/// @sa QP::QTimeEvt.
|
|
///
|
|
#ifndef Q_SPY
|
|
void QF::tickX_(uint_fast8_t const tickRate)
|
|
#else
|
|
void QF::tickX_(uint_fast8_t const tickRate, void const * const sender)
|
|
#endif
|
|
{
|
|
QTimeEvt *prev = &timeEvtHead_[tickRate];
|
|
QF_CRIT_STAT_
|
|
|
|
QF_CRIT_ENTRY_();
|
|
|
|
QS_BEGIN_NOCRIT_(QS_QF_TICK, static_cast<void*>(0), static_cast<void*>(0))
|
|
QS_TEC_(static_cast<QTimeEvtCtr>(++prev->m_ctr)); // tick ctr
|
|
QS_U8_(static_cast<uint8_t>(tickRate)); // tick rate
|
|
QS_END_NOCRIT_()
|
|
|
|
// scan the linked-list of time events at this rate...
|
|
for (;;) {
|
|
QTimeEvt *t = prev->m_next; // advance down the time evt. list
|
|
|
|
// end of the list?
|
|
if (t == static_cast<QTimeEvt *>(0)) {
|
|
|
|
// any new time events armed since the last run of QF::tickX_()?
|
|
if (timeEvtHead_[tickRate].m_act != static_cast<void *>(0)) {
|
|
|
|
// sanity check
|
|
Q_ASSERT_ID(110, prev != static_cast<QTimeEvt *>(0));
|
|
prev->m_next = QF::timeEvtHead_[tickRate].toTimeEvt();
|
|
timeEvtHead_[tickRate].m_act = static_cast<void *>(0);
|
|
t = prev->m_next; // switch to the new list
|
|
}
|
|
else {
|
|
break; // all currently armed time evts. processed
|
|
}
|
|
}
|
|
|
|
// time event scheduled for removal?
|
|
if (t->m_ctr == static_cast<QTimeEvtCtr>(0)) {
|
|
prev->m_next = t->m_next;
|
|
t->refCtr_ &= static_cast<uint8_t>(0x7F); // mark as unlinked
|
|
// do NOT advance the prev pointer
|
|
QF_CRIT_EXIT_(); // exit crit. section to reduce latency
|
|
|
|
// prevent merging critical sections, see NOTE1 below
|
|
QF_CRIT_EXIT_NOP();
|
|
}
|
|
else {
|
|
--t->m_ctr;
|
|
|
|
// is time evt about to expire?
|
|
if (t->m_ctr == static_cast<QTimeEvtCtr>(0)) {
|
|
QActive *act = t->toActive(); // temporary for volatile
|
|
|
|
// periodic time evt?
|
|
if (t->m_interval != static_cast<QTimeEvtCtr>(0)) {
|
|
t->m_ctr = t->m_interval; // rearm the time event
|
|
prev = t; // advance to this time event
|
|
}
|
|
// one-shot time event: automatically disarm
|
|
else {
|
|
prev->m_next = t->m_next;
|
|
|
|
// mark as unlinked
|
|
t->refCtr_ &= static_cast<uint8_t>(0x7F);
|
|
// do NOT advance the prev pointer
|
|
|
|
QS_BEGIN_NOCRIT_(QS_QF_TIMEEVT_AUTO_DISARM,
|
|
QS::priv_.locFilter[QS::TE_OBJ], t)
|
|
QS_OBJ_(t); // this time event object
|
|
QS_OBJ_(act); // the target AO
|
|
QS_U8_(static_cast<uint8_t>(tickRate)); // tick rate
|
|
QS_END_NOCRIT_()
|
|
}
|
|
|
|
QS_BEGIN_NOCRIT_(QS_QF_TIMEEVT_POST,
|
|
QS::priv_.locFilter[QS::TE_OBJ], t)
|
|
QS_TIME_(); // timestamp
|
|
QS_OBJ_(t); // the time event object
|
|
QS_SIG_(t->sig); // signal of this time event
|
|
QS_OBJ_(act); // the target AO
|
|
QS_U8_(static_cast<uint8_t>(tickRate)); // tick rate
|
|
QS_END_NOCRIT_()
|
|
|
|
QF_CRIT_EXIT_(); // exit crit. section before posting
|
|
|
|
(void)act->POST(t, sender); // asserts if queue overflows
|
|
}
|
|
else {
|
|
prev = t; // advance to this time event
|
|
QF_CRIT_EXIT_(); // exit crit. section to reduce latency
|
|
|
|
// prevent merging critical sections, see NOTE1 below
|
|
QF_CRIT_EXIT_NOP();
|
|
}
|
|
}
|
|
QF_CRIT_ENTRY_(); // re-enter crit. section to continue
|
|
}
|
|
QF_CRIT_EXIT_();
|
|
}
|
|
|
|
//****************************************************************************
|
|
// NOTE1:
|
|
// In some QF ports the critical section exit takes effect only on the next
|
|
// machine instruction. If this case, the next instruction is another entry
|
|
// to a critical section, the critical section won't be really exited, but
|
|
// rather the two adjacent critical sections would be merged.
|
|
//
|
|
// The QF_CRIT_EXIT_NOP() macro contains minimal code required
|
|
// to prevent such merging of critical sections in QF ports,
|
|
// in which it can occur.
|
|
|
|
|
|
//****************************************************************************
|
|
/// @description
|
|
/// Find out if any time events are armed at the given clock tick rate.
|
|
///
|
|
/// @param[in] tickRate system clock tick rate to find out about.
|
|
///
|
|
/// @returns 'true' if no time events are armed at the given tick rate and
|
|
/// 'false' otherwise.
|
|
///
|
|
/// @note This function should be called in critical section.
|
|
///
|
|
bool QF::noTimeEvtsActiveX(uint_fast8_t const tickRate) {
|
|
/// @pre the tick rate must be in range
|
|
Q_REQUIRE_ID(200, tickRate < static_cast<uint_fast8_t>(QF_MAX_TICK_RATE));
|
|
|
|
bool inactive;
|
|
if (timeEvtHead_[tickRate].m_next == static_cast<QTimeEvt *>(0)) {
|
|
inactive = false;
|
|
}
|
|
else if (timeEvtHead_[tickRate].m_act == static_cast<void *>(0)) {
|
|
inactive = false;
|
|
}
|
|
else {
|
|
inactive = true;
|
|
}
|
|
return inactive;
|
|
}
|
|
|
|
//****************************************************************************
|
|
/// @description
|
|
/// When creating a time event, you must commit it to a specific active object
|
|
/// @p act, tick rate @p tickRate and event signal @p sgnl. You cannot change
|
|
/// these attributes later.
|
|
///
|
|
/// @param[in] act pointer to the active object associated with this
|
|
/// time event. The time event will post itself to this AO.
|
|
/// @param[in] sgnl signal to associate with this time event.
|
|
/// @param[in] tickRate system tick rate to associate with this time event.
|
|
///
|
|
QTimeEvt::QTimeEvt(QActive * const act,
|
|
enum_t const sgnl, uint_fast8_t const tickRate)
|
|
:
|
|
#ifdef Q_EVT_CTOR
|
|
QEvt(static_cast<QSignal>(sgnl)),
|
|
#endif
|
|
m_next(static_cast<QTimeEvt *>(0)),
|
|
m_act(act),
|
|
m_ctr(static_cast<QTimeEvtCtr>(0)),
|
|
m_interval(static_cast<QTimeEvtCtr>(0))
|
|
{
|
|
/// @pre The signal must be valid and the tick rate in range
|
|
Q_REQUIRE_ID(300, (sgnl >= Q_USER_SIG)
|
|
&& (tickRate < static_cast<uint_fast8_t>(QF_MAX_TICK_RATE)));
|
|
|
|
#ifndef Q_EVT_CTOR
|
|
sig = static_cast<QSignal>(sgnl); // set QEvt::sig of this time event
|
|
#endif
|
|
|
|
// Setting the POOL_ID event attribute to zero is correct only for
|
|
// events not allocated from event pools, which must be the case
|
|
// for Time Events.
|
|
//
|
|
poolId_ = static_cast<uint8_t>(0);
|
|
|
|
// The reference counter attribute is not used in static events,
|
|
// so for the Time Events it is reused to hold the tickRate in the
|
|
// bits [0..6] and the linkedFlag in the MSB (bit [7]). The linkedFlag
|
|
// is 0 for time events unlinked from any list and 1 otherwise.
|
|
//
|
|
refCtr_ = static_cast<uint8_t>(tickRate);
|
|
}
|
|
|
|
//****************************************************************************
|
|
/// @note
|
|
/// private default ctor for internal use only
|
|
///
|
|
QTimeEvt::QTimeEvt()
|
|
:
|
|
#ifdef Q_EVT_CTOR
|
|
QEvt(static_cast<QSignal>(0)),
|
|
#endif // Q_EVT_CTOR
|
|
|
|
m_next(static_cast<QTimeEvt *>(0)),
|
|
m_act(static_cast<QActive *>(0)),
|
|
m_ctr(static_cast<QTimeEvtCtr>(0)),
|
|
m_interval(static_cast<QTimeEvtCtr>(0))
|
|
{
|
|
#ifndef Q_EVT_CTOR
|
|
sig = static_cast<QSignal>(0);
|
|
#endif // Q_EVT_CTOR
|
|
|
|
// Setting the POOL_ID event attribute to zero is correct only for
|
|
// events not allocated from event pools, which must be the case
|
|
// for Time Events.
|
|
//
|
|
poolId_ = static_cast<uint8_t>(0); // not from any event pool
|
|
|
|
// The reference counter attribute is not used in static events,
|
|
// so for the Time Events it is reused to hold the tickRate in the
|
|
// bits [0..6] and the linkedFlag in the MSB (bit [7]). The linkedFlag
|
|
// is 0 for time events unlinked from any list and 1 otherwise.
|
|
//
|
|
refCtr_ = static_cast<uint8_t>(0); // default rate 0
|
|
}
|
|
|
|
//****************************************************************************
|
|
/// @description
|
|
/// Arms a time event to fire in a specified number of clock ticks and with
|
|
/// a specified interval. If the interval is zero, the time event is armed for
|
|
/// one shot ('one-shot' time event). The time event gets directly posted
|
|
/// (using the FIFO policy) into the event queue of the host active object.
|
|
///
|
|
/// @param[in] nTicks number of clock ticks (at the associated rate)
|
|
/// to rearm the time event with.
|
|
/// @param[in] interval interval (in clock ticks) for periodic time event.
|
|
///
|
|
/// @note After posting, a one-shot time event gets automatically disarmed
|
|
/// while a periodic time event (interval != 0) is automatically re-armed.
|
|
///
|
|
/// @note A time event can be disarmed at any time by calling
|
|
/// QP::QTimeEvt::disarm(). Also, a time event can be re-armed to fire in a
|
|
/// different number of clock ticks by calling the QP::QTimeEvt::rearm()
|
|
/// function.
|
|
///
|
|
/// @usage
|
|
/// The following example shows how to arm a one-shot time event from a state
|
|
/// machine of an active object:
|
|
/// @include qf_state.cpp
|
|
///
|
|
void QTimeEvt::armX(QTimeEvtCtr const nTicks, QTimeEvtCtr const interval) {
|
|
uint_fast8_t tickRate = static_cast<uint_fast8_t>(refCtr_)
|
|
& static_cast<uint_fast8_t>(0x7F);
|
|
QTimeEvtCtr cntr = m_ctr; // temporary to hold volatile
|
|
QF_CRIT_STAT_
|
|
|
|
/// @pre the host AO must be valid, time evnet must be disarmed,
|
|
/// number of clock ticks cannot be zero, and the signal must be valid.
|
|
///
|
|
Q_REQUIRE_ID(400, (m_act != static_cast<void *>(0))
|
|
&& (cntr == static_cast<QTimeEvtCtr>(0))
|
|
&& (nTicks != static_cast<QTimeEvtCtr>(0))
|
|
&& (tickRate < static_cast<uint_fast8_t>(QF_MAX_TICK_RATE))
|
|
&& (static_cast<enum_t>(sig) >= Q_USER_SIG));
|
|
|
|
QF_CRIT_ENTRY_();
|
|
m_ctr = nTicks;
|
|
m_interval = interval;
|
|
|
|
// is the time event unlinked?
|
|
// NOTE: For the duration of a single clock tick of the specified tick
|
|
// rate a time event can be disarmed and yet still linked into the list,
|
|
// because un-linking is performed exclusively in the QF_tickX() function.
|
|
//
|
|
if ((refCtr_ & static_cast<uint8_t>(0x80)) == static_cast<uint8_t>(0)) {
|
|
refCtr_ |= static_cast<uint8_t>(0x80); // mark as linked
|
|
|
|
// The time event is initially inserted into the separate
|
|
// "freshly armed" link list based on QF::timeEvtHead_[tickRate].act.
|
|
// Only later, inside the QF::tickX() function, the "freshly armed"
|
|
// list is appended to the main list of armed time events based on
|
|
// QF::timeEvtHead_[tickRate].next. Again, this is to keep any
|
|
// changes to the main list exclusively inside the QF::tickX()
|
|
// function.
|
|
//
|
|
m_next = QF::timeEvtHead_[tickRate].toTimeEvt();
|
|
QF::timeEvtHead_[tickRate].m_act = this;
|
|
}
|
|
|
|
QS_BEGIN_NOCRIT_(QS_QF_TIMEEVT_ARM, QS::priv_.locFilter[QS::TE_OBJ], this)
|
|
QS_TIME_(); // timestamp
|
|
QS_OBJ_(this); // this time event object
|
|
QS_OBJ_(m_act); // the active object
|
|
QS_TEC_(nTicks); // the number of ticks
|
|
QS_TEC_(interval); // the interval
|
|
QS_U8_(static_cast<uint8_t>(tickRate)); // tick rate
|
|
QS_END_NOCRIT_()
|
|
|
|
QF_CRIT_EXIT_();
|
|
}
|
|
|
|
//****************************************************************************
|
|
/// @description
|
|
/// Disarm the time event so it can be safely reused.
|
|
///
|
|
/// @returns 'true' if the time event was truly disarmed, that is, it
|
|
/// was running. The return of 'false' means that the time event was
|
|
/// not truly disarmed because it was not running. The 'false' return is only
|
|
/// possible for one-shot time events that have been automatically disarmed
|
|
/// upon expiration. In this case the 'false' return means that the time event
|
|
/// has already been posted or published and should be expected in the
|
|
/// active object's state machine.
|
|
///
|
|
/// @note there is no harm in disarming an already disarmed time event
|
|
////
|
|
bool QTimeEvt::disarm(void) {
|
|
QF_CRIT_STAT_
|
|
QF_CRIT_ENTRY_();
|
|
bool wasArmed;
|
|
|
|
// is the time event actually armed?
|
|
if (m_ctr != static_cast<QTimeEvtCtr>(0)) {
|
|
wasArmed = true;
|
|
|
|
QS_BEGIN_NOCRIT_(QS_QF_TIMEEVT_DISARM,
|
|
QS::priv_.locFilter[QS::TE_OBJ], this)
|
|
QS_TIME_(); // timestamp
|
|
QS_OBJ_(this); // this time event object
|
|
QS_OBJ_(m_act); // the target AO
|
|
QS_TEC_(m_ctr); // the number of ticks
|
|
QS_TEC_(m_interval); // the interval
|
|
// tick rate
|
|
QS_U8_(static_cast<uint8_t>(refCtr_& static_cast<uint8_t>(0x7F)));
|
|
QS_END_NOCRIT_()
|
|
|
|
m_ctr = static_cast<QTimeEvtCtr>(0); // schedule removal from the list
|
|
}
|
|
// the time event was already not running
|
|
else {
|
|
wasArmed = false;
|
|
|
|
QS_BEGIN_NOCRIT_(QS_QF_TIMEEVT_DISARM_ATTEMPT,
|
|
QS::priv_.locFilter[QS::TE_OBJ], this)
|
|
QS_TIME_(); // timestamp
|
|
QS_OBJ_(this); // this time event object
|
|
QS_OBJ_(m_act); // the target AO
|
|
// tick rate
|
|
QS_U8_(static_cast<uint8_t>(refCtr_& static_cast<uint8_t>(0x7F)));
|
|
QS_END_NOCRIT_()
|
|
}
|
|
QF_CRIT_EXIT_();
|
|
return wasArmed;
|
|
}
|
|
|
|
/****************************************************************************/
|
|
/**
|
|
* @description
|
|
* Rearms a time event with a new number of clock ticks. This function can
|
|
* be used to adjust the current period of a periodic time event or to
|
|
* prevent a one-shot time event from expiring (e.g., a watchdog time event).
|
|
* Rearming a periodic timer leaves the interval unchanged and is a convenient
|
|
* method to adjust the phasing of a periodic time event.
|
|
*
|
|
* @param[in] nTicks number of clock ticks (at the associated rate)
|
|
* to rearm the time event with.
|
|
*
|
|
* @returns 'true' if the time event was running as it
|
|
* was re-armed. The 'false' return means that the time event was
|
|
* not truly rearmed because it was not running. The 'false' return is only
|
|
* possible for one-shot time events that have been automatically disarmed
|
|
* upon expiration. In this case the 'false' return means that the time event
|
|
* has already been posted or published and should be expected in the
|
|
* active object's state machine.
|
|
*/
|
|
bool QTimeEvt::rearm(QTimeEvtCtr const nTicks) {
|
|
uint_fast8_t tickRate = static_cast<uint_fast8_t>(refCtr_)
|
|
& static_cast<uint_fast8_t>(0x7F);
|
|
QF_CRIT_STAT_
|
|
|
|
/// @pre AO must be valid, tick rate must be in range, nTicks must not
|
|
/// be zero, and the signal of this time event must be valid
|
|
///
|
|
Q_REQUIRE_ID(600, (m_act != static_cast<void *>(0))
|
|
&& (tickRate < static_cast<uint_fast8_t>(QF_MAX_TICK_RATE))
|
|
&& (nTicks != static_cast<QTimeEvtCtr>(0))
|
|
&& (static_cast<enum_t>(sig) >= Q_USER_SIG));
|
|
|
|
QF_CRIT_ENTRY_();
|
|
bool isArmed;
|
|
|
|
// is the time evt not running? */
|
|
if (m_ctr == static_cast<QTimeEvtCtr>(0)) {
|
|
isArmed = false;
|
|
|
|
// is the time event unlinked?
|
|
// NOTE: For a duration of a single clock tick of the specified
|
|
// tick rate a time event can be disarmed and yet still linked into
|
|
// the list, because unlinking is performed exclusively in the
|
|
// QF::tickX() function.
|
|
//
|
|
if ((refCtr_ & static_cast<uint8_t>(0x80))
|
|
== static_cast<uint8_t>(0))
|
|
{
|
|
refCtr_ |= static_cast<uint8_t>(0x80); // mark as linked
|
|
|
|
// The time event is initially inserted into the separate
|
|
// "freshly armed" list based on QF_timeEvtHead_[tickRate].act.
|
|
// Only later, inside the QF_tickX() function, the "freshly armed"
|
|
// list is appended to the main list of armed time events based on
|
|
// QF_timeEvtHead_[tickRate].next. Again, this is to keep any
|
|
// changes to the main list exclusively inside the QF_tickX()
|
|
// function.
|
|
//
|
|
m_next = QF::timeEvtHead_[tickRate].toTimeEvt();
|
|
QF::timeEvtHead_[tickRate].m_act = this;
|
|
}
|
|
}
|
|
// the time event is armed
|
|
else {
|
|
isArmed = true;
|
|
}
|
|
m_ctr = nTicks; // re-load the tick counter (shift the phasing)
|
|
|
|
QS_BEGIN_NOCRIT_(QS_QF_TIMEEVT_REARM,
|
|
QS::priv_.locFilter[QS::TE_OBJ], this)
|
|
QS_TIME_(); // timestamp
|
|
QS_OBJ_(this); // this time event object
|
|
QS_OBJ_(m_act); // the target AO
|
|
QS_TEC_(m_ctr); // the number of ticks
|
|
QS_TEC_(m_interval); // the interval
|
|
QS_U8_(static_cast<uint8_t>(tickRate)); // the tick rate
|
|
if (isArmed) {
|
|
QS_U8_(static_cast<uint8_t>(1)); // status: armed
|
|
}
|
|
else {
|
|
QS_U8_(static_cast<uint8_t>(0)); // status: disarmed
|
|
}
|
|
QS_END_NOCRIT_()
|
|
|
|
QF_CRIT_EXIT_();
|
|
return isArmed;
|
|
}
|
|
|
|
//****************************************************************************
|
|
/// @description
|
|
/// Useful for checking how many clock ticks (at the tick rate associated
|
|
/// with the time event) remain until the time event expires.
|
|
///
|
|
/// @returns The current value of the down-counter for an armed time event
|
|
/// or 0 for an unarmed time event.
|
|
///
|
|
/// /note The function is thread-safe.
|
|
///
|
|
QTimeEvtCtr QTimeEvt::ctr(void) const {
|
|
QF_CRIT_STAT_
|
|
|
|
QF_CRIT_ENTRY_();
|
|
QTimeEvtCtr ret = m_ctr;
|
|
|
|
QS_BEGIN_NOCRIT_(QS_QF_TIMEEVT_CTR, QS::priv_.locFilter[QS::TE_OBJ], this)
|
|
QS_TIME_(); // timestamp
|
|
QS_OBJ_(this); // this time event object
|
|
QS_OBJ_(m_act); // the target AO
|
|
QS_TEC_(ret); // the current counter
|
|
QS_TEC_(m_interval); // the interval
|
|
QS_U8_(refCtr_ & static_cast<uint8_t>(0x7F)); // tick rate
|
|
QS_END_NOCRIT_()
|
|
|
|
QF_CRIT_EXIT_();
|
|
return ret;
|
|
}
|
|
|
|
} // namespace QP
|