Browse Source

EVMJIT: Better object cache and other improvements

cl-refactor
Paweł Bylica 10 years ago
parent
commit
272a0bd6fc
  1. 2
      evmjit/CMakeLists.txt
  2. 19
      evmjit/libevmjit/BasicBlock.cpp
  3. 23
      evmjit/libevmjit/BasicBlock.h
  4. 40
      evmjit/libevmjit/Cache.cpp
  5. 1
      evmjit/libevmjit/Cache.h
  6. 127
      evmjit/libevmjit/Compiler.cpp
  7. 2
      evmjit/libevmjit/Compiler.h
  8. 11
      evmjit/libevmjit/ExecutionEngine.cpp
  9. 4
      evmjit/libevmjit/Ext.cpp
  10. 4
      evmjit/libevmjit/Ext.h
  11. 2
      evmjit/libevmjit/GasMeter.cpp
  12. 40
      evmjit/libevmjit/Instruction.cpp
  13. 6
      evmjit/libevmjit/Instruction.h
  14. 19
      evmjit/libevmjit/Utils.cpp

2
evmjit/CMakeLists.txt

@ -19,6 +19,8 @@ else()
set(LLVM_LIBS "-lLLVMBitWriter -lLLVMX86CodeGen -lLLVMSelectionDAG -lLLVMAsmPrinter -lLLVMCodeGen -lLLVMScalarOpts -lLLVMInstCombine -lLLVMTransformUtils -lLLVMipa -lLLVMAnalysis -lLLVMX86AsmParser -lLLVMX86Desc -lLLVMX86Info -lLLVMX86AsmPrinter -lLLVMX86Utils -lLLVMMCJIT -lLLVMTarget -lLLVMRuntimeDyld -lLLVMObject -lLLVMMCParser -lLLVMBitReader -lLLVMExecutionEngine -lLLVMMC -lLLVMCore -lLLVMSupport -lz -lpthread -lffi -ltinfo -ldl -lm")
endif()
add_definitions(-D_SCL_SECURE_NO_WARNINGS) # LLVM needs it on Windows
# Boost
find_package(Boost REQUIRED)

19
evmjit/libevmjit/BasicBlock.cpp

@ -20,20 +20,21 @@ namespace jit
const char* BasicBlock::NamePrefix = "Instr.";
BasicBlock::BasicBlock(ProgramCounter _beginInstIdx, ProgramCounter _endInstIdx, llvm::Function* _mainFunc, llvm::IRBuilder<>& _builder) :
m_beginInstIdx(_beginInstIdx),
m_endInstIdx(_endInstIdx),
m_llvmBB(llvm::BasicBlock::Create(_mainFunc->getContext(), {NamePrefix, std::to_string(_beginInstIdx)}, _mainFunc)),
BasicBlock::BasicBlock(bytes::const_iterator _begin, bytes::const_iterator _end, llvm::Function* _mainFunc, llvm::IRBuilder<>& _builder, bool isJumpDest) :
m_begin(_begin),
m_end(_end),
// TODO: Add begin index to name
m_llvmBB(llvm::BasicBlock::Create(_mainFunc->getContext(), NamePrefix, _mainFunc)),
m_stack(*this),
m_builder(_builder)
m_builder(_builder),
m_isJumpDest(isJumpDest)
{}
BasicBlock::BasicBlock(std::string _name, llvm::Function* _mainFunc, llvm::IRBuilder<>& _builder) :
m_beginInstIdx(0),
m_endInstIdx(0),
BasicBlock::BasicBlock(std::string _name, llvm::Function* _mainFunc, llvm::IRBuilder<>& _builder, bool isJumpDest) :
m_llvmBB(llvm::BasicBlock::Create(_mainFunc->getContext(), _name, _mainFunc)),
m_stack(*this),
m_builder(_builder)
m_builder(_builder),
m_isJumpDest(isJumpDest)
{}
BasicBlock::LocalStack::LocalStack(BasicBlock& _owner) :

23
evmjit/libevmjit/BasicBlock.h

@ -1,9 +1,7 @@
#pragma once
#include <vector>
#include <llvm/IR/BasicBlock.h>
#include "Common.h"
#include "Stack.h"
namespace dev
@ -52,23 +50,22 @@ public:
BasicBlock& m_bblock;
};
/// Basic block name prefix. The rest is beging instruction index.
/// Basic block name prefix. The rest is instruction index.
static const char* NamePrefix;
explicit BasicBlock(ProgramCounter _beginInstIdx, ProgramCounter _endInstIdx, llvm::Function* _mainFunc, llvm::IRBuilder<>& _builder);
explicit BasicBlock(std::string _name, llvm::Function* _mainFunc, llvm::IRBuilder<>& _builder);
explicit BasicBlock(bytes::const_iterator _begin, bytes::const_iterator _end, llvm::Function* _mainFunc, llvm::IRBuilder<>& _builder, bool isJumpDest);
explicit BasicBlock(std::string _name, llvm::Function* _mainFunc, llvm::IRBuilder<>& _builder, bool isJumpDest);
BasicBlock(const BasicBlock&) = delete;
void operator=(const BasicBlock&) = delete;
operator llvm::BasicBlock*() { return m_llvmBB; }
operator llvm::BasicBlock*() { return m_llvmBB; } // TODO: Remove it
llvm::BasicBlock* llvm() { return m_llvmBB; }
ProgramCounter begin() { return m_beginInstIdx; }
ProgramCounter end() { return m_endInstIdx; }
bytes::const_iterator begin() { return m_begin; }
bytes::const_iterator end() { return m_end; }
bool isJumpDest() const { return m_isJumpDest; }
void markAsJumpDest() { m_isJumpDest = true; }
LocalStack& localStack() { return m_stack; }
@ -85,8 +82,8 @@ public:
void dump(std::ostream& os, bool _dotOutput = false);
private:
ProgramCounter const m_beginInstIdx;
ProgramCounter const m_endInstIdx;
bytes::const_iterator const m_begin;
bytes::const_iterator const m_end;
llvm::BasicBlock* const m_llvmBB;
@ -115,7 +112,7 @@ private:
/// Is the basic block a valid jump destination.
/// JUMPDEST is the first instruction of the basic block.
bool m_isJumpDest = false;
bool const m_isJumpDest = false;
};
}

40
evmjit/libevmjit/Cache.cpp

@ -3,6 +3,7 @@
#include <cassert>
#include <iostream>
#include <llvm/IR/Module.h>
#include <llvm/IR/LLVMContext.h>
#include <llvm/Support/Path.h>
#include <llvm/Support/FileSystem.h>
#include <llvm/Support/raw_os_ostream.h>
@ -23,6 +24,32 @@ ObjectCache* Cache::getObjectCache()
return &objectCache;
}
namespace
{
llvm::MemoryBuffer* lastObject;
}
std::unique_ptr<llvm::Module> Cache::getObject(std::string const& id)
{
assert(!lastObject);
llvm::SmallString<256> cachePath;
llvm::sys::path::system_temp_directory(false, cachePath);
llvm::sys::path::append(cachePath, "evm_objs", id);
if (auto r = llvm::MemoryBuffer::getFile(cachePath.str(), -1, false))
lastObject = llvm::MemoryBuffer::getMemBufferCopy(r.get()->getBuffer());
else if (r.getError() != std::make_error_code(std::errc::no_such_file_or_directory))
std::cerr << r.getError().message(); // TODO: Add log
if (lastObject) // if object found create fake module
{
auto module = std::unique_ptr<llvm::Module>(new llvm::Module(id, llvm::getGlobalContext()));
auto mainFuncType = llvm::FunctionType::get(llvm::IntegerType::get(llvm::getGlobalContext(), 32), {}, false);
auto func = llvm::Function::Create(mainFuncType, llvm::Function::ExternalLinkage, id, module.get());
}
return nullptr;
}
void ObjectCache::notifyObjectCompiled(llvm::Module const* _module, llvm::MemoryBuffer const* _object)
{
@ -43,16 +70,9 @@ void ObjectCache::notifyObjectCompiled(llvm::Module const* _module, llvm::Memory
llvm::MemoryBuffer* ObjectCache::getObject(llvm::Module const* _module)
{
auto&& id = _module->getModuleIdentifier();
llvm::SmallString<256> cachePath;
llvm::sys::path::system_temp_directory(false, cachePath);
llvm::sys::path::append(cachePath, "evm_objs", id);
if (auto r = llvm::MemoryBuffer::getFile(cachePath.str(), -1, false))
return llvm::MemoryBuffer::getMemBufferCopy(r.get()->getBuffer());
else if (r.getError() != std::make_error_code(std::errc::no_such_file_or_directory))
std::cerr << r.getError().message(); // TODO: Add log
return nullptr;
auto o = lastObject;
lastObject = nullptr;
return o;
}
}

1
evmjit/libevmjit/Cache.h

@ -33,6 +33,7 @@ class Cache
{
public:
static ObjectCache* getObjectCache();
static std::unique_ptr<llvm::Module> getObject(std::string const& id);
};
}

127
evmjit/libevmjit/Compiler.cpp

@ -40,77 +40,69 @@ Compiler::Compiler(Options const& _options):
void Compiler::createBasicBlocks(bytes const& _bytecode)
{
// FIXME: Simplify this algorithm. All can be done in one pass
std::set<ProgramCounter> splitPoints; // Sorted collections of instruction indices where basic blocks start/end
std::vector<ProgramCounter> indirectJumpTargets;
splitPoints.insert(0); // First basic block
for (auto curr = _bytecode.begin(); curr != _bytecode.end(); ++curr)
/// Helper function that skips push data and finds next iterator (can be the end)
auto skipPushDataAndGetNext = [](bytes::const_iterator _curr, bytes::const_iterator _end)
{
ProgramCounter currentPC = curr - _bytecode.begin();
auto inst = Instruction(*curr);
switch (inst)
{
case Instruction::ANY_PUSH:
{
readPushData(curr, _bytecode.end());
break;
}
static const auto push1 = static_cast<size_t>(Instruction::PUSH1);
static const auto push32 = static_cast<size_t>(Instruction::PUSH32);
size_t offset = 1;
if (*_curr >= push1 && *_curr <= push32)
offset += std::min<size_t>(*_curr - push1 + 1, (_end - _curr) - 1);
return _curr + offset;
};
auto begin = _bytecode.begin();
bool nextJumpDest = false;
for (auto curr = begin, next = begin; curr != _bytecode.end(); curr = next)
{
next = skipPushDataAndGetNext(curr, _bytecode.end());
case Instruction::JUMPDEST:
bool isEnd = false;
switch (Instruction(*curr))
{
// A basic block starts here.
splitPoints.insert(currentPC);
indirectJumpTargets.push_back(currentPC);
break;
}
case Instruction::JUMP:
case Instruction::JUMPI:
case Instruction::RETURN:
case Instruction::STOP:
case Instruction::SUICIDE:
{
// Create a basic block starting at the following instruction.
if (curr + 1 < _bytecode.end())
splitPoints.insert(currentPC + 1);
isEnd = true;
break;
case Instruction::JUMPDEST:
nextJumpDest = true;
break;
}
default:
break;
}
}
for (auto it = splitPoints.cbegin(); it != splitPoints.cend();)
{
auto beginInstIdx = *it;
++it;
auto endInstIdx = it != splitPoints.cend() ? *it : _bytecode.size();
basicBlocks.emplace(std::piecewise_construct, std::forward_as_tuple(beginInstIdx), std::forward_as_tuple(beginInstIdx, endInstIdx, m_mainFunc, m_builder));
assert(next <= _bytecode.end());
if (next == _bytecode.end() || Instruction(*next) == Instruction::JUMPDEST)
isEnd = true;
if (isEnd)
{
auto beginIdx = begin - _bytecode.begin();
m_basicBlocks.emplace(std::piecewise_construct, std::forward_as_tuple(beginIdx),
std::forward_as_tuple(begin, next, m_mainFunc, m_builder, nextJumpDest));
nextJumpDest = false;
begin = next;
}
}
m_stopBB = llvm::BasicBlock::Create(m_mainFunc->getContext(), "Stop", m_mainFunc);
for (auto it = indirectJumpTargets.cbegin(); it != indirectJumpTargets.cend(); ++it)
basicBlocks.find(*it)->second.markAsJumpDest();
}
llvm::BasicBlock* Compiler::getJumpTableBlock()
{
if (!m_jumpTableBlock)
{
m_jumpTableBlock.reset(new BasicBlock("JumpTable", m_mainFunc, m_builder));
m_jumpTableBlock.reset(new BasicBlock("JumpTable", m_mainFunc, m_builder, true));
InsertPointGuard g{m_builder};
m_builder.SetInsertPoint(m_jumpTableBlock->llvm());
auto dest = m_jumpTableBlock->localStack().pop();
auto switchInstr = m_builder.CreateSwitch(dest, getBadJumpBlock());
for (auto&& p : basicBlocks)
for (auto&& p : m_basicBlocks)
{
if (p.second.isJumpDest())
switchInstr->addCase(Constant::get(p.first), p.second.llvm());
@ -123,7 +115,7 @@ llvm::BasicBlock* Compiler::getBadJumpBlock()
{
if (!m_badJumpBlock)
{
m_badJumpBlock.reset(new BasicBlock("BadJump", m_mainFunc, m_builder));
m_badJumpBlock.reset(new BasicBlock("BadJump", m_mainFunc, m_builder, true));
InsertPointGuard g{m_builder};
m_builder.SetInsertPoint(m_badJumpBlock->llvm());
m_builder.CreateRet(Constant::get(ReturnCode::BadJumpDestination));
@ -155,14 +147,14 @@ std::unique_ptr<llvm::Module> Compiler::compile(bytes const& _bytecode, std::str
Stack stack(m_builder, runtimeManager);
Arith256 arith(m_builder);
m_builder.CreateBr(basicBlocks.begin()->second);
m_builder.CreateBr(m_basicBlocks.empty() ? m_stopBB : m_basicBlocks.begin()->second);
for (auto basicBlockPairIt = basicBlocks.begin(); basicBlockPairIt != basicBlocks.end(); ++basicBlockPairIt)
for (auto basicBlockPairIt = m_basicBlocks.begin(); basicBlockPairIt != m_basicBlocks.end(); ++basicBlockPairIt)
{
auto& basicBlock = basicBlockPairIt->second;
auto iterCopy = basicBlockPairIt;
++iterCopy;
auto nextBasicBlock = (iterCopy != basicBlocks.end()) ? iterCopy->second.llvm() : nullptr;
auto nextBasicBlock = (iterCopy != m_basicBlocks.end()) ? iterCopy->second.llvm() : nullptr;
compileBasicBlock(basicBlock, _bytecode, runtimeManager, arith, memory, ext, gasMeter, nextBasicBlock);
}
@ -178,7 +170,7 @@ std::unique_ptr<llvm::Module> Compiler::compile(bytes const& _bytecode, std::str
if (m_options.optimizeStack)
{
std::vector<BasicBlock*> blockList;
for (auto& entry : basicBlocks)
for (auto& entry : m_basicBlocks)
blockList.push_back(&entry.second);
if (m_jumpTableBlock)
@ -189,7 +181,7 @@ std::unique_ptr<llvm::Module> Compiler::compile(bytes const& _bytecode, std::str
dumpCFGifRequired("blocks-opt.dot");
}
for (auto& entry : basicBlocks)
for (auto& entry : m_basicBlocks)
entry.second.synchronizeLocalStack(stack);
if (m_jumpTableBlock)
m_jumpTableBlock->synchronizeLocalStack(stack);
@ -219,9 +211,9 @@ void Compiler::compileBasicBlock(BasicBlock& _basicBlock, bytes const& _bytecode
m_builder.SetInsertPoint(_basicBlock.llvm());
auto& stack = _basicBlock.localStack();
for (auto currentPC = _basicBlock.begin(); currentPC != _basicBlock.end(); ++currentPC)
for (auto it = _basicBlock.begin(); it != _basicBlock.end(); ++it)
{
auto inst = static_cast<Instruction>(_bytecode[currentPC]);
auto inst = Instruction(*it);
_gasMeter.count(inst);
@ -483,10 +475,7 @@ void Compiler::compileBasicBlock(BasicBlock& _basicBlock, bytes const& _bytecode
case Instruction::ANY_PUSH:
{
auto curr = _bytecode.begin() + currentPC; // TODO: replace currentPC with iterator
auto value = readPushData(curr, _bytecode.end());
currentPC = curr - _bytecode.begin();
auto value = readPushData(it, _basicBlock.end());
stack.push(Constant::get(value));
break;
}
@ -561,24 +550,16 @@ void Compiler::compileBasicBlock(BasicBlock& _basicBlock, bytes const& _bytecode
if (auto constant = llvm::dyn_cast<llvm::ConstantInt>(target))
{
auto&& c = constant->getValue();
if (c.ult(_bytecode.size()))
{
auto v = c.getZExtValue();
auto it = basicBlocks.find(v);
if (it != basicBlocks.end() && it->second.isJumpDest())
targetBlock = it->second.llvm();
}
if (!targetBlock)
targetBlock = getBadJumpBlock();
auto targetIdx = c.getActiveBits() <= 64 ? c.getZExtValue() : -1;
auto it = m_basicBlocks.find(targetIdx);
targetBlock = (it != m_basicBlocks.end() && it->second.isJumpDest()) ? it->second.llvm() : getBadJumpBlock();
}
// TODO: Improve; check for constants
if (inst == Instruction::JUMP)
{
if (targetBlock)
{
// The target address is computed at compile time,
// just pop it without looking...
m_builder.CreateBr(targetBlock);
}
else
@ -614,7 +595,7 @@ void Compiler::compileBasicBlock(BasicBlock& _basicBlock, bytes const& _bytecode
case Instruction::PC:
{
auto value = Constant::get(currentPC);
auto value = Constant::get(it - _bytecode.begin());
stack.push(value);
break;
}
@ -832,13 +813,13 @@ void Compiler::removeDeadBlocks()
do
{
sthErased = false;
for (auto it = basicBlocks.begin(); it != basicBlocks.end();)
for (auto it = m_basicBlocks.begin(); it != m_basicBlocks.end();)
{
auto llvmBB = it->second.llvm();
if (llvm::pred_begin(llvmBB) == llvm::pred_end(llvmBB))
{
llvmBB->eraseFromParent();
basicBlocks.erase(it++);
m_basicBlocks.erase(it++);
sthErased = true;
}
else
@ -866,7 +847,7 @@ void Compiler::dumpCFGtoStream(std::ostream& _out)
<< " entry [share=record, label=\"entry block\"];\n";
std::vector<BasicBlock*> blocks;
for (auto& pair : basicBlocks)
for (auto& pair : m_basicBlocks)
blocks.push_back(&pair.second);
if (m_jumpTableBlock)
blocks.push_back(m_jumpTableBlock.get());
@ -905,7 +886,7 @@ void Compiler::dumpCFGtoStream(std::ostream& _out)
void Compiler::dump()
{
for (auto& entry : basicBlocks)
for (auto& entry : m_basicBlocks)
entry.second.dump();
if (m_jumpTableBlock != nullptr)
m_jumpTableBlock->dump();

2
evmjit/libevmjit/Compiler.h

@ -69,7 +69,7 @@ private:
llvm::IRBuilder<> m_builder;
/// Maps a program counter pc to a basic block that starts at pc (if any).
std::map<ProgramCounter, BasicBlock> basicBlocks;
std::map<ProgramCounter, BasicBlock> m_basicBlocks;
/// Stop basic block - terminates execution with STOP code (0)
llvm::BasicBlock* m_stopBB = nullptr;

11
evmjit/libevmjit/ExecutionEngine.cpp

@ -74,7 +74,13 @@ ReturnCode ExecutionEngine::run(bytes const& _code, RuntimeData* _data, Env* _en
}
else
{
auto module = Compiler({}).compile(_code, mainFuncName);
bool objectCacheEnabled = true;
auto objectCache = objectCacheEnabled ? Cache::getObjectCache() : nullptr;
std::unique_ptr<llvm::Module> module;
if (objectCache)
module = Cache::getObject(mainFuncName);
if (!module)
module = Compiler({}).compile(_code, mainFuncName);
//module->dump();
if (!ee)
{
@ -100,7 +106,8 @@ ReturnCode ExecutionEngine::run(bytes const& _code, RuntimeData* _data, Env* _en
module.release(); // Successfully created llvm::ExecutionEngine takes ownership of the module
memoryManager.release(); // and memory manager
ee->setObjectCache(Cache::getObjectCache());
if (objectCache)
ee->setObjectCache(objectCache);
entryFuncPtr = (EntryFuncPtr)ee->getFunctionAddress(mainFuncName);
}
else

4
evmjit/libevmjit/Ext.cpp

@ -22,7 +22,9 @@ namespace jit
Ext::Ext(RuntimeManager& _runtimeManager, Memory& _memoryMan):
RuntimeHelper(_runtimeManager),
m_memoryMan(_memoryMan)
m_memoryMan(_memoryMan),
m_funcs{},
m_argAllocas{}
{
m_size = m_builder.CreateAlloca(Type::Size, nullptr, "env.size");
}

4
evmjit/libevmjit/Ext.h

@ -65,8 +65,8 @@ private:
llvm::Value* m_size;
llvm::Value* m_data = nullptr;
std::array<llvm::Function*, sizeOf<EnvFunc>::value> m_funcs = {{}};
std::array<llvm::Value*, 8> m_argAllocas = {{}};
std::array<llvm::Function*, sizeOf<EnvFunc>::value> m_funcs;
std::array<llvm::Value*, 8> m_argAllocas;
size_t m_argCounter = 0;
llvm::CallInst* createCall(EnvFunc _funcId, std::initializer_list<llvm::Value*> const& _args);

2
evmjit/libevmjit/GasMeter.cpp

@ -41,7 +41,7 @@ uint64_t const c_logDataGas = 1;
uint64_t const c_logTopicGas = 32;
uint64_t const c_copyGas = 1;
uint64_t getStepCost(Instruction inst) // TODO: Add this function to FeeSructure (pull request submitted)
uint64_t getStepCost(Instruction inst)
{
switch (inst)
{

40
evmjit/libevmjit/Instruction.cpp

@ -0,0 +1,40 @@
#include "Instruction.h"
#include <llvm/ADT/APInt.h>
namespace dev
{
namespace eth
{
namespace jit
{
llvm::APInt readPushData(bytes::const_iterator& _curr, bytes::const_iterator _end)
{
auto pushInst = *_curr;
assert(Instruction(pushInst) >= Instruction::PUSH1 && Instruction(pushInst) <= Instruction::PUSH32);
auto numBytes = pushInst - static_cast<size_t>(Instruction::PUSH1) + 1;
llvm::APInt value(256, 0);
++_curr; // Point the data
for (decltype(numBytes) i = 0; i < numBytes; ++i)
{
byte b = (_curr != _end) ? *_curr++ : 0;
value <<= 8;
value |= b;
}
--_curr; // Point the last real byte read
return value;
}
void skipPushData(bytes::const_iterator& _curr, bytes::const_iterator _end)
{
auto pushInst = *_curr;
assert(Instruction(pushInst) >= Instruction::PUSH1 && Instruction(pushInst) <= Instruction::PUSH32);
auto numBytes = pushInst - static_cast<size_t>(Instruction::PUSH1) + 1;
--_end;
for (decltype(numBytes) i = 0; i < numBytes && _curr < _end; ++i, ++_curr) {}
}
}
}
}

6
evmjit/libevmjit/Instruction.h

@ -160,9 +160,13 @@ enum class Instruction: uint8_t
/// Reads PUSH data from pointed fragment of bytecode and constructs number out of it
/// Reading out of bytecode means reading 0
/// @param _curr is updates and points the last real byte read
/// @param _curr is updated and points the last real byte read
llvm::APInt readPushData(bytes::const_iterator& _curr, bytes::const_iterator _end);
/// Skips PUSH data in pointed fragment of bytecode.
/// @param _curr is updated and points the last real byte skipped
void skipPushData(bytes::const_iterator& _curr, bytes::const_iterator _end);
#define ANY_PUSH PUSH1: \
case Instruction::PUSH2: \
case Instruction::PUSH3: \

19
evmjit/libevmjit/Utils.cpp

@ -1,7 +1,5 @@
#include <llvm/ADT/APInt.h>
#include "Utils.h"
#include "Instruction.h"
namespace dev
{
@ -37,23 +35,6 @@ i256 eth2llvm(u256 _u)
return i;
}
llvm::APInt readPushData(bytes::const_iterator& _curr, bytes::const_iterator _end)
{
auto pushInst = *_curr;
assert(Instruction(pushInst) >= Instruction::PUSH1 && Instruction(pushInst) <= Instruction::PUSH32);
auto numBytes = pushInst - static_cast<size_t>(Instruction::PUSH1) + 1;
llvm::APInt value(256, 0);
++_curr; // Point the data
for (decltype(numBytes) i = 0; i < numBytes; ++i)
{
byte b = (_curr != _end) ? *_curr++ : 0;
value <<= 8;
value |= b;
}
--_curr; // Point the last real byte read
return value;
}
}
}
}

Loading…
Cancel
Save