CJentzsch
10 years ago
50 changed files with 1387 additions and 256 deletions
@ -0,0 +1,181 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<ui version="4.0"> |
||||
|
<class>GasPricing</class> |
||||
|
<widget class="QDialog" name="GasPricing"> |
||||
|
<property name="geometry"> |
||||
|
<rect> |
||||
|
<x>0</x> |
||||
|
<y>0</y> |
||||
|
<width>416</width> |
||||
|
<height>286</height> |
||||
|
</rect> |
||||
|
</property> |
||||
|
<property name="windowTitle"> |
||||
|
<string>Gas Pricing</string> |
||||
|
</property> |
||||
|
<layout class="QGridLayout" name="gridLayout"> |
||||
|
<item row="3" column="0" colspan="2"> |
||||
|
<spacer name="verticalSpacer"> |
||||
|
<property name="orientation"> |
||||
|
<enum>Qt::Vertical</enum> |
||||
|
</property> |
||||
|
<property name="sizeHint" stdset="0"> |
||||
|
<size> |
||||
|
<width>398</width> |
||||
|
<height>45</height> |
||||
|
</size> |
||||
|
</property> |
||||
|
</spacer> |
||||
|
</item> |
||||
|
<item row="4" column="0" colspan="2"> |
||||
|
<widget class="QLabel" name="label5_3"> |
||||
|
<property name="text"> |
||||
|
<string>&Bid: The minimum grice of gas that is accepting into a block which we mine.</string> |
||||
|
</property> |
||||
|
<property name="alignment"> |
||||
|
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> |
||||
|
</property> |
||||
|
<property name="wordWrap"> |
||||
|
<bool>true</bool> |
||||
|
</property> |
||||
|
<property name="buddy"> |
||||
|
<cstring>bidValue</cstring> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item row="1" column="0"> |
||||
|
<widget class="QSpinBox" name="askValue"> |
||||
|
<property name="suffix"> |
||||
|
<string/> |
||||
|
</property> |
||||
|
<property name="maximum"> |
||||
|
<number>430000000</number> |
||||
|
</property> |
||||
|
<property name="value"> |
||||
|
<number>0</number> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item row="10" column="0" colspan="2"> |
||||
|
<layout class="QHBoxLayout" name="horizontalLayout"> |
||||
|
<item> |
||||
|
<spacer name="horizontalSpacer"> |
||||
|
<property name="orientation"> |
||||
|
<enum>Qt::Horizontal</enum> |
||||
|
</property> |
||||
|
<property name="sizeHint" stdset="0"> |
||||
|
<size> |
||||
|
<width>40</width> |
||||
|
<height>20</height> |
||||
|
</size> |
||||
|
</property> |
||||
|
</spacer> |
||||
|
</item> |
||||
|
<item> |
||||
|
<widget class="QPushButton" name="close"> |
||||
|
<property name="text"> |
||||
|
<string>Close</string> |
||||
|
</property> |
||||
|
<property name="default"> |
||||
|
<bool>true</bool> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
</layout> |
||||
|
</item> |
||||
|
<item row="0" column="0" colspan="2"> |
||||
|
<widget class="QLabel" name="label5_2"> |
||||
|
<property name="text"> |
||||
|
<string>&Ask: The minimum grice of gas that is accepting into a block which we mine.</string> |
||||
|
</property> |
||||
|
<property name="alignment"> |
||||
|
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> |
||||
|
</property> |
||||
|
<property name="wordWrap"> |
||||
|
<bool>true</bool> |
||||
|
</property> |
||||
|
<property name="buddy"> |
||||
|
<cstring>askValue</cstring> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item row="1" column="1"> |
||||
|
<widget class="QComboBox" name="askUnits"/> |
||||
|
</item> |
||||
|
<item row="5" column="0"> |
||||
|
<widget class="QSpinBox" name="bidValue"> |
||||
|
<property name="suffix"> |
||||
|
<string/> |
||||
|
</property> |
||||
|
<property name="maximum"> |
||||
|
<number>430000000</number> |
||||
|
</property> |
||||
|
<property name="value"> |
||||
|
<number>0</number> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item row="5" column="1"> |
||||
|
<widget class="QComboBox" name="bidUnits"/> |
||||
|
</item> |
||||
|
<item row="7" column="0" colspan="2"> |
||||
|
<spacer name="verticalSpacer_2"> |
||||
|
<property name="orientation"> |
||||
|
<enum>Qt::Vertical</enum> |
||||
|
</property> |
||||
|
<property name="sizeHint" stdset="0"> |
||||
|
<size> |
||||
|
<width>20</width> |
||||
|
<height>40</height> |
||||
|
</size> |
||||
|
</property> |
||||
|
</spacer> |
||||
|
</item> |
||||
|
<item row="9" column="0" colspan="2"> |
||||
|
<spacer name="verticalSpacer_3"> |
||||
|
<property name="orientation"> |
||||
|
<enum>Qt::Vertical</enum> |
||||
|
</property> |
||||
|
<property name="sizeHint" stdset="0"> |
||||
|
<size> |
||||
|
<width>20</width> |
||||
|
<height>40</height> |
||||
|
</size> |
||||
|
</property> |
||||
|
</spacer> |
||||
|
</item> |
||||
|
<item row="8" column="0" colspan="2"> |
||||
|
<spacer name="verticalSpacer_4"> |
||||
|
<property name="orientation"> |
||||
|
<enum>Qt::Vertical</enum> |
||||
|
</property> |
||||
|
<property name="sizeHint" stdset="0"> |
||||
|
<size> |
||||
|
<width>20</width> |
||||
|
<height>40</height> |
||||
|
</size> |
||||
|
</property> |
||||
|
</spacer> |
||||
|
</item> |
||||
|
</layout> |
||||
|
</widget> |
||||
|
<resources/> |
||||
|
<connections> |
||||
|
<connection> |
||||
|
<sender>close</sender> |
||||
|
<signal>clicked()</signal> |
||||
|
<receiver>GasPricing</receiver> |
||||
|
<slot>accept()</slot> |
||||
|
<hints> |
||||
|
<hint type="sourcelabel"> |
||||
|
<x>387</x> |
||||
|
<y>264</y> |
||||
|
</hint> |
||||
|
<hint type="destinationlabel"> |
||||
|
<x>240</x> |
||||
|
<y>262</y> |
||||
|
</hint> |
||||
|
</hints> |
||||
|
</connection> |
||||
|
</connections> |
||||
|
</ui> |
@ -1,4 +1,6 @@ |
|||||
[ |
[ |
||||
{ "name": "eth_getWork", "params": [], "order": [], "returns": []}, |
{ "name": "eth_getWork", "params": [], "order": [], "returns": []}, |
||||
{ "name": "eth_submitWork", "params": ["", "", ""], "order": [], "returns": true} |
{ "name": "eth_submitWork", "params": ["", "", ""], "order": [], "returns": true} |
||||
|
{ "name": "eth_awaitNewWork", "params": [], "order": [], "returns": []}, |
||||
|
{ "name": "eth_progress", "params": [], "order": [], "returns": true} |
||||
] |
] |
||||
|
@ -0,0 +1,225 @@ |
|||||
|
/*
|
||||
|
This file is part of cpp-ethereum. |
||||
|
|
||||
|
cpp-ethereum 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 3 of the License, or |
||||
|
(at your option) any later version. |
||||
|
|
||||
|
cpp-ethereum 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 cpp-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
*/ |
||||
|
/** @file ConstantOptimiser.cpp
|
||||
|
* @author Christian <c@ethdev.com> |
||||
|
* @date 2015 |
||||
|
*/ |
||||
|
|
||||
|
#include "libevmasm/ConstantOptimiser.h" |
||||
|
#include <libevmasm/Assembly.h> |
||||
|
#include <libevmasm/GasMeter.h> |
||||
|
#include <libevmcore/Params.h> |
||||
|
using namespace std; |
||||
|
using namespace dev; |
||||
|
using namespace dev::eth; |
||||
|
|
||||
|
unsigned ConstantOptimisationMethod::optimiseConstants( |
||||
|
bool _isCreation, |
||||
|
size_t _runs, |
||||
|
Assembly& _assembly, |
||||
|
AssemblyItems& _items |
||||
|
) |
||||
|
{ |
||||
|
unsigned optimisations = 0; |
||||
|
map<AssemblyItem, size_t> pushes; |
||||
|
for (AssemblyItem const& item: _items) |
||||
|
if (item.type() == Push) |
||||
|
pushes[item]++; |
||||
|
for (auto it: pushes) |
||||
|
{ |
||||
|
AssemblyItem const& item = it.first; |
||||
|
if (item.data() < 0x100) |
||||
|
continue; |
||||
|
Params params; |
||||
|
params.multiplicity = it.second; |
||||
|
params.isCreation = _isCreation; |
||||
|
params.runs = _runs; |
||||
|
LiteralMethod lit(params, item.data()); |
||||
|
bigint literalGas = lit.gasNeeded(); |
||||
|
CodeCopyMethod copy(params, item.data()); |
||||
|
bigint copyGas = copy.gasNeeded(); |
||||
|
ComputeMethod compute(params, item.data()); |
||||
|
bigint computeGas = compute.gasNeeded(); |
||||
|
if (copyGas < literalGas && copyGas < computeGas) |
||||
|
{ |
||||
|
copy.execute(_assembly, _items); |
||||
|
optimisations++; |
||||
|
} |
||||
|
else if (computeGas < literalGas && computeGas < copyGas) |
||||
|
{ |
||||
|
compute.execute(_assembly, _items); |
||||
|
optimisations++; |
||||
|
} |
||||
|
} |
||||
|
return optimisations; |
||||
|
} |
||||
|
|
||||
|
bigint ConstantOptimisationMethod::simpleRunGas(AssemblyItems const& _items) |
||||
|
{ |
||||
|
bigint gas = 0; |
||||
|
for (AssemblyItem const& item: _items) |
||||
|
if (item.type() == Push) |
||||
|
gas += GasMeter::runGas(Instruction::PUSH1); |
||||
|
else if (item.type() == Operation) |
||||
|
gas += GasMeter::runGas(item.instruction()); |
||||
|
return gas; |
||||
|
} |
||||
|
|
||||
|
bigint ConstantOptimisationMethod::dataGas(bytes const& _data) const |
||||
|
{ |
||||
|
if (m_params.isCreation) |
||||
|
{ |
||||
|
bigint gas; |
||||
|
for (auto b: _data) |
||||
|
gas += b ? c_txDataNonZeroGas : c_txDataZeroGas; |
||||
|
return gas; |
||||
|
} |
||||
|
else |
||||
|
return c_createDataGas * dataSize(); |
||||
|
} |
||||
|
|
||||
|
size_t ConstantOptimisationMethod::bytesRequired(AssemblyItems const& _items) |
||||
|
{ |
||||
|
size_t size = 0; |
||||
|
for (AssemblyItem const& item: _items) |
||||
|
size += item.bytesRequired(3); // assume 3 byte addresses
|
||||
|
return size; |
||||
|
} |
||||
|
|
||||
|
void ConstantOptimisationMethod::replaceConstants( |
||||
|
AssemblyItems& _items, |
||||
|
AssemblyItems const& _replacement |
||||
|
) const |
||||
|
{ |
||||
|
assertThrow(_items.size() > 0, OptimizerException, ""); |
||||
|
for (size_t i = 0; i < _items.size(); ++i) |
||||
|
{ |
||||
|
if (_items.at(i) != AssemblyItem(m_value)) |
||||
|
continue; |
||||
|
_items[i] = _replacement[0]; |
||||
|
_items.insert(_items.begin() + i + 1, _replacement.begin() + 1, _replacement.end()); |
||||
|
i += _replacement.size() - 1; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
bigint LiteralMethod::gasNeeded() |
||||
|
{ |
||||
|
return combineGas( |
||||
|
simpleRunGas({Instruction::PUSH1}), |
||||
|
// PUSHX plus data
|
||||
|
(m_params.isCreation ? c_txDataNonZeroGas : c_createDataGas) + dataGas(), |
||||
|
0 |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
CodeCopyMethod::CodeCopyMethod(Params const& _params, u256 const& _value): |
||||
|
ConstantOptimisationMethod(_params, _value) |
||||
|
{ |
||||
|
m_copyRoutine = AssemblyItems{ |
||||
|
u256(0), |
||||
|
Instruction::DUP1, |
||||
|
Instruction::MLOAD, // back up memory
|
||||
|
u256(32), |
||||
|
AssemblyItem(PushData, u256(1) << 16), // has to be replaced
|
||||
|
Instruction::DUP4, |
||||
|
Instruction::CODECOPY, |
||||
|
Instruction::DUP2, |
||||
|
Instruction::MLOAD, |
||||
|
Instruction::SWAP2, |
||||
|
Instruction::MSTORE |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
bigint CodeCopyMethod::gasNeeded() |
||||
|
{ |
||||
|
return combineGas( |
||||
|
// Run gas: we ignore memory increase costs
|
||||
|
simpleRunGas(m_copyRoutine) + c_copyGas, |
||||
|
// Data gas for copy routines: Some bytes are zero, but we ignore them.
|
||||
|
bytesRequired(m_copyRoutine) * (m_params.isCreation ? c_txDataNonZeroGas : c_createDataGas), |
||||
|
// Data gas for data itself
|
||||
|
dataGas(toBigEndian(m_value)) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
void CodeCopyMethod::execute(Assembly& _assembly, AssemblyItems& _items) |
||||
|
{ |
||||
|
bytes data = toBigEndian(m_value); |
||||
|
m_copyRoutine[4] = _assembly.newData(data); |
||||
|
replaceConstants(_items, m_copyRoutine); |
||||
|
} |
||||
|
|
||||
|
AssemblyItems ComputeMethod::findRepresentation(u256 const& _value) |
||||
|
{ |
||||
|
if (_value < 0x10000) |
||||
|
// Very small value, not worth computing
|
||||
|
return AssemblyItems{_value}; |
||||
|
else if (dev::bytesRequired(~_value) < dev::bytesRequired(_value)) |
||||
|
// Negated is shorter to represent
|
||||
|
return findRepresentation(~_value) + AssemblyItems{Instruction::NOT}; |
||||
|
else |
||||
|
{ |
||||
|
// Decompose value into a * 2**k + b where abs(b) << 2**k
|
||||
|
// Is not always better, try literal and decomposition method.
|
||||
|
AssemblyItems routine{u256(_value)}; |
||||
|
bigint bestGas = gasNeeded(routine); |
||||
|
for (unsigned bits = 255; bits > 8; --bits) |
||||
|
{ |
||||
|
unsigned gapDetector = unsigned(_value >> (bits - 8)) & 0x1ff; |
||||
|
if (gapDetector != 0xff && gapDetector != 0x100) |
||||
|
continue; |
||||
|
|
||||
|
u256 powerOfTwo = u256(1) << bits; |
||||
|
u256 upperPart = _value >> bits; |
||||
|
bigint lowerPart = _value & (powerOfTwo - 1); |
||||
|
if (abs(powerOfTwo - lowerPart) < lowerPart) |
||||
|
lowerPart = lowerPart - powerOfTwo; // make it negative
|
||||
|
if (abs(lowerPart) >= (powerOfTwo >> 8)) |
||||
|
continue; |
||||
|
|
||||
|
AssemblyItems newRoutine; |
||||
|
if (lowerPart != 0) |
||||
|
newRoutine += findRepresentation(u256(abs(lowerPart))); |
||||
|
newRoutine += AssemblyItems{u256(bits), u256(2), Instruction::EXP}; |
||||
|
if (upperPart != 1 && upperPart != 0) |
||||
|
newRoutine += findRepresentation(upperPart) + AssemblyItems{Instruction::MUL}; |
||||
|
if (lowerPart > 0) |
||||
|
newRoutine += AssemblyItems{Instruction::ADD}; |
||||
|
else if (lowerPart < 0) |
||||
|
newRoutine.push_back(Instruction::SUB); |
||||
|
|
||||
|
bigint newGas = gasNeeded(newRoutine); |
||||
|
if (newGas < bestGas) |
||||
|
{ |
||||
|
bestGas = move(newGas); |
||||
|
routine = move(newRoutine); |
||||
|
} |
||||
|
} |
||||
|
return routine; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
bigint ComputeMethod::gasNeeded(AssemblyItems const& _routine) |
||||
|
{ |
||||
|
size_t numExps = count(_routine.begin(), _routine.end(), Instruction::EXP); |
||||
|
return combineGas( |
||||
|
simpleRunGas(_routine) + numExps * (c_expGas + c_expByteGas), |
||||
|
// Data gas for routine: Some bytes are zero, but we ignore them.
|
||||
|
bytesRequired(_routine) * (m_params.isCreation ? c_txDataNonZeroGas : c_createDataGas), |
||||
|
0 |
||||
|
); |
||||
|
} |
@ -0,0 +1,147 @@ |
|||||
|
/*
|
||||
|
This file is part of cpp-ethereum. |
||||
|
|
||||
|
cpp-ethereum 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 3 of the License, or |
||||
|
(at your option) any later version. |
||||
|
|
||||
|
cpp-ethereum 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 cpp-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
*/ |
||||
|
/** @file ConstantOptimiser.cpp
|
||||
|
* @author Christian <c@ethdev.com> |
||||
|
* @date 2015 |
||||
|
*/ |
||||
|
|
||||
|
#pragma once |
||||
|
|
||||
|
#include <vector> |
||||
|
#include <libdevcore/CommonData.h> |
||||
|
#include <libdevcore/CommonIO.h> |
||||
|
|
||||
|
namespace dev |
||||
|
{ |
||||
|
namespace eth |
||||
|
{ |
||||
|
|
||||
|
class AssemblyItem; |
||||
|
using AssemblyItems = std::vector<AssemblyItem>; |
||||
|
class Assembly; |
||||
|
|
||||
|
/**
|
||||
|
* Abstract base class for one way to change how constants are represented in the code. |
||||
|
*/ |
||||
|
class ConstantOptimisationMethod |
||||
|
{ |
||||
|
public: |
||||
|
/// Tries to optimised how constants are represented in the source code and modifies
|
||||
|
/// @a _assembly and its @a _items.
|
||||
|
/// @returns zero if no optimisations could be performed.
|
||||
|
static unsigned optimiseConstants( |
||||
|
bool _isCreation, |
||||
|
size_t _runs, |
||||
|
Assembly& _assembly, |
||||
|
AssemblyItems& _items |
||||
|
); |
||||
|
|
||||
|
struct Params |
||||
|
{ |
||||
|
bool isCreation; ///< Whether this is called during contract creation or runtime.
|
||||
|
size_t runs; ///< Estimated number of calls per opcode oven the lifetime of the contract.
|
||||
|
size_t multiplicity; ///< Number of times the constant appears in the code.
|
||||
|
}; |
||||
|
|
||||
|
explicit ConstantOptimisationMethod(Params const& _params, u256 const& _value): |
||||
|
m_params(_params), m_value(_value) {} |
||||
|
virtual bigint gasNeeded() = 0; |
||||
|
virtual void execute(Assembly& _assembly, AssemblyItems& _items) = 0; |
||||
|
|
||||
|
protected: |
||||
|
size_t dataSize() const { return std::max<size_t>(1, dev::bytesRequired(m_value)); } |
||||
|
|
||||
|
/// @returns the run gas for the given items ignoring special gas costs
|
||||
|
static bigint simpleRunGas(AssemblyItems const& _items); |
||||
|
/// @returns the gas needed to store the given data literally
|
||||
|
bigint dataGas(bytes const& _data) const; |
||||
|
/// @returns the gas needed to store the value literally
|
||||
|
bigint dataGas() const { return dataGas(toCompactBigEndian(m_value, 1)); } |
||||
|
static size_t bytesRequired(AssemblyItems const& _items); |
||||
|
/// @returns the combined estimated gas usage taking @a m_params into account.
|
||||
|
bigint combineGas( |
||||
|
bigint const& _runGas, |
||||
|
bigint const& _repeatedDataGas, |
||||
|
bigint const& _uniqueDataGas |
||||
|
) |
||||
|
{ |
||||
|
// _runGas is not multiplied by _multiplicity because the runs are "per opcode"
|
||||
|
return m_params.runs * _runGas + m_params.multiplicity * _repeatedDataGas + _uniqueDataGas; |
||||
|
} |
||||
|
|
||||
|
/// Replaces the constant by the code given in @a _replacement.
|
||||
|
void replaceConstants(AssemblyItems& _items, AssemblyItems const& _replacement) const; |
||||
|
|
||||
|
Params m_params; |
||||
|
u256 const& m_value; |
||||
|
}; |
||||
|
|
||||
|
/**
|
||||
|
* Optimisation method that pushes the constant to the stack literally. This is the default method, |
||||
|
* i.e. executing it does not alter the Assembly. |
||||
|
*/ |
||||
|
class LiteralMethod: public ConstantOptimisationMethod |
||||
|
{ |
||||
|
public: |
||||
|
explicit LiteralMethod(Params const& _params, u256 const& _value): |
||||
|
ConstantOptimisationMethod(_params, _value) {} |
||||
|
virtual bigint gasNeeded() override; |
||||
|
virtual void execute(Assembly&, AssemblyItems&) override {} |
||||
|
}; |
||||
|
|
||||
|
/**
|
||||
|
* Method that stores the data in the .data section of the code and copies it to the stack. |
||||
|
*/ |
||||
|
class CodeCopyMethod: public ConstantOptimisationMethod |
||||
|
{ |
||||
|
public: |
||||
|
explicit CodeCopyMethod(Params const& _params, u256 const& _value); |
||||
|
virtual bigint gasNeeded() override; |
||||
|
virtual void execute(Assembly& _assembly, AssemblyItems& _items) override; |
||||
|
|
||||
|
protected: |
||||
|
AssemblyItems m_copyRoutine; |
||||
|
}; |
||||
|
|
||||
|
/**
|
||||
|
* Method that tries to compute the constant. |
||||
|
*/ |
||||
|
class ComputeMethod: public ConstantOptimisationMethod |
||||
|
{ |
||||
|
public: |
||||
|
explicit ComputeMethod(Params const& _params, u256 const& _value): |
||||
|
ConstantOptimisationMethod(_params, _value) |
||||
|
{ |
||||
|
m_routine = findRepresentation(m_value); |
||||
|
} |
||||
|
|
||||
|
virtual bigint gasNeeded() override { return gasNeeded(m_routine); } |
||||
|
virtual void execute(Assembly&, AssemblyItems& _items) override |
||||
|
{ |
||||
|
replaceConstants(_items, m_routine); |
||||
|
} |
||||
|
|
||||
|
protected: |
||||
|
/// Tries to recursively find a way to compute @a _value.
|
||||
|
AssemblyItems findRepresentation(u256 const& _value); |
||||
|
bigint gasNeeded(AssemblyItems const& _routine); |
||||
|
|
||||
|
AssemblyItems m_routine; |
||||
|
}; |
||||
|
|
||||
|
} |
||||
|
} |
Loading…
Reference in new issue