Change directory structure
This commit is contained in:
345
luprex/cpp/core/drivenengine.hpp
Normal file
345
luprex/cpp/core/drivenengine.hpp
Normal file
@@ -0,0 +1,345 @@
|
||||
//////////////////////////////////////////////////////////////
|
||||
//
|
||||
// DrivenEngine
|
||||
//
|
||||
// This module embodies the idea of an "event-driven game engine." The
|
||||
// DrivenEngine module provides two APIs: the engine-side API, and the
|
||||
// driver-side API.
|
||||
//
|
||||
// The engine-side API looks like a typical collection of I/O primitives. It
|
||||
// includes methods to open sockets, read and write sockets, read lua source,
|
||||
// get the clock, and so forth.
|
||||
//
|
||||
// But in reality, these I/O functions don't ever call operating system
|
||||
// functions like "read" or "write" or "connect." They don't call the operating
|
||||
// system at all - not even indirectly, through a wrapper. Therefore, they
|
||||
// can't really do any I/O. When you use one of these I/O functions to (say)
|
||||
// write some data to a communication channel, the only thing that happens is
|
||||
// that the data is put into a buffer. The actual transmission of the data
|
||||
// happens elsewhere, in what is called the "Driver." Likewise, when you use
|
||||
// one of these I/O functions to read data, it only returns data that was
|
||||
// previously stored by the "Driver."
|
||||
//
|
||||
// The "Driver" is a module that implements the actual I/O. It is highly
|
||||
// OS-dependent code, because it contains code to manipulate sockets, time
|
||||
// clocks, and the like.
|
||||
//
|
||||
// From the perspective of the driver, the DrivenEngine is a C++ object that
|
||||
// acts like a state machine. This state machine is driven forward by I/O
|
||||
// events. The DrivenEngine provides an API where the driver can feed in these
|
||||
// I/O events.
|
||||
//
|
||||
// Notice that the usual call graph is inverted: in most application programs,
|
||||
// the application calls the operating system to do I/O. But when using class
|
||||
// DrivenEngine, it's the other way around: the driver calls into class
|
||||
// DrivenEngine to drive it forward. I/O routines drive computation.
|
||||
//
|
||||
// So the upshot of all this is that the DrivenEngine is a deterministic state
|
||||
// machine, free of all OS-specific code.
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef DRIVENENGINE_HPP
|
||||
#define DRIVENENGINE_HPP
|
||||
|
||||
#include "wrap-string.hpp"
|
||||
#include "wrap-vector.hpp"
|
||||
|
||||
#include <ostream>
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
#include "util.hpp"
|
||||
#include "streambuffer.hpp"
|
||||
#include "enginewrapper.hpp"
|
||||
|
||||
class DrivenEngine;
|
||||
using UniqueDrivenEngine = std::unique_ptr<DrivenEngine>;
|
||||
using DrivenEngineMaker = UniqueDrivenEngine (*)();
|
||||
using DrivenEngineInitializer = void (*)();
|
||||
|
||||
class Channel : public eng::opnew {
|
||||
public:
|
||||
// Get the buffers associated with this channel.
|
||||
//
|
||||
StreamBuffer *out() { return sb_out_.get(); }
|
||||
StreamBuffer *in() { return sb_in_.get(); }
|
||||
|
||||
// The channel ID. These are reused.
|
||||
//
|
||||
int chid() const { return chid_; }
|
||||
|
||||
// If this is a socket connection, the receiver's port number.
|
||||
//
|
||||
int port() const { return port_; }
|
||||
|
||||
// If this is an outgoing socket connection, get the target host.
|
||||
//
|
||||
const eng::string &target() const { return target_; }
|
||||
|
||||
// True if the remote closed the connection, or a failure occurred.
|
||||
//
|
||||
bool closed() const { return closed_; }
|
||||
|
||||
// Get the channel's error message.
|
||||
//
|
||||
// If this is an empty string, there is no error. If this is set,
|
||||
// then the channel is also closed.
|
||||
//
|
||||
const eng::string &error() const { return error_; }
|
||||
|
||||
// Set the prompt for readline mode.
|
||||
//
|
||||
void set_prompt(const eng::string &prompt);
|
||||
|
||||
// Do not construct your own Channels. Instead,
|
||||
// use methods of class DrivenEngine like new_outgoing_channel.
|
||||
// Channels are referenced by shared_ptr. You can
|
||||
// release your shared_ptr at any time.
|
||||
//
|
||||
Channel(DrivenEngine *de, int chid, int port, const eng::string &target, bool stop);
|
||||
~Channel() {};
|
||||
|
||||
private:
|
||||
// Constructor is deliberately private. Use
|
||||
// DrivenEngine::new_outgoing_channel to create outgoing socket channels.
|
||||
//
|
||||
|
||||
void feed_readline(std::string_view data);
|
||||
std::string_view peek_outgoing() const;
|
||||
void sent_outgoing(int nbytes);
|
||||
void erase_command();
|
||||
void echo_command();
|
||||
void pump_readline();
|
||||
|
||||
private:
|
||||
static const int READLINE_MAX=512;
|
||||
int chid_;
|
||||
|
||||
// These are the in/out buffers presented to the user.
|
||||
std::shared_ptr<StreamBuffer> sb_in_;
|
||||
std::shared_ptr<StreamBuffer> sb_out_;
|
||||
|
||||
// If this is stdio, we inject tty echoes into the output stream.
|
||||
// This buffer holds the users output interleaved with the tty echoes.
|
||||
// In any other channel, this is just another pointer to sb_out.
|
||||
std::shared_ptr<StreamBuffer> sb_drvout_;
|
||||
|
||||
int port_;
|
||||
bool closed_;
|
||||
eng::string error_;
|
||||
eng::string target_;
|
||||
bool stop_driver_;
|
||||
|
||||
// Readline stuff. Only used on channel 0 (stdio).
|
||||
eng::string desired_command_;
|
||||
eng::string current_command_;
|
||||
eng::string desired_prompt_;
|
||||
eng::string current_prompt_;
|
||||
char readline_lastc_;
|
||||
|
||||
friend class DrivenEngine;
|
||||
};
|
||||
|
||||
using SharedChannel = std::shared_ptr<Channel>;
|
||||
|
||||
class DrivenEngine : public eng::opnew {
|
||||
public:
|
||||
//////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Build the named engine
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
static UniqueDrivenEngine make(std::string_view name);
|
||||
|
||||
static void print_usage(std::ostream &strm, std::string_view progname);
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
//
|
||||
// The following methods are the 'engine' side of the pipe.
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
// The init callback. You may override this in a subclass.
|
||||
// This will be called once at program initialization.
|
||||
//
|
||||
virtual void event_init(int argc, char *argv[]) {}
|
||||
|
||||
// The update callback. You may override this in a subclass.
|
||||
// This will be called whenever anything changes.
|
||||
//
|
||||
virtual void event_update() {}
|
||||
|
||||
// Specify the set of listening ports.
|
||||
// This can only be used during the init routine.
|
||||
//
|
||||
void listen_port(int port);
|
||||
|
||||
// Get the current time.
|
||||
//
|
||||
// DRIVER: This returns the time most recently stored by the driver
|
||||
// using drv_set_clock.
|
||||
//
|
||||
double get_clock();
|
||||
|
||||
// Create a channel and open an outgoing connection. The channel creation
|
||||
// always succeeds. You can write to the channel immediately. You can
|
||||
// read, too, but of course there won't be anything in the incoming buffer
|
||||
// yet. In future update events, data will show up in the incoming buffer,
|
||||
// and will have been sent from the outgoing buffer. In future update
|
||||
// events, the channel may get closed by the remote. If the connection
|
||||
// fails (say, the remote host doesn't exist), then the Channel will get
|
||||
// closed with an error.
|
||||
//
|
||||
// DRIVER: The channel object is created instantly, but it does nothing
|
||||
// until the driver notices the new channel. The driver is responsible for
|
||||
// actually opening the connection and relaying data into the channel using
|
||||
// drv_get_target, drv_peek_outgoing, drv_sent_outgoing, drv_recv_incoming.
|
||||
//
|
||||
SharedChannel new_outgoing_channel(const eng::string &target);
|
||||
|
||||
// Create a new channel from any pending incoming connection. If there is no
|
||||
// incoming connection, returns nullptr.
|
||||
//
|
||||
// DRIVER: The driver must be hardwired to know what ports to listen on.
|
||||
// When the driver notices a new incoming connection, it calls
|
||||
// drv_notify_accept, which triggers the creation of the channel. The
|
||||
// channel is put into the incoming channel queue, which is fetched by this
|
||||
// method. The driver is responsible for relaying data into the channel
|
||||
// using drv_get_target, drv_peek_outgoing, drv_sent_outgoing,
|
||||
// drv_recv_incoming.
|
||||
//
|
||||
SharedChannel new_incoming_channel();
|
||||
|
||||
// Obtain the stdio channel. There is only one stdio channel.
|
||||
//
|
||||
// DRIVER: the stdio channel is created automatically when the DrivenEngine
|
||||
// is created. The driver is responsible for relaying data into the channel
|
||||
// using drv_get_target, drv_peek_outgoing, drv_sent_outgoing,
|
||||
// drv_recv_incoming.
|
||||
//
|
||||
SharedChannel get_stdio_channel();
|
||||
|
||||
// Obtain the output buffer of the stdio channel as an ostream.
|
||||
//
|
||||
std::ostream &stdostream() { return get_stdio_channel()->out()->ostream(); }
|
||||
|
||||
// Fetches the lua source, and takes ownership of it. The DrivenEngine
|
||||
// no longer contains the source after calling this.
|
||||
//
|
||||
util::LuaSourcePtr get_lua_source();
|
||||
|
||||
// Rescan the lua source directory. The lua source directory is read once,
|
||||
// automatically, at engine creation time. If you want to read it again,
|
||||
// you must trigger a rescan. The rescan is not instantaneous.
|
||||
//
|
||||
// DRIVER: this merely sets a flag, which the driver will notice later,
|
||||
// causing the driver to update the lua source.
|
||||
//
|
||||
void rescan_lua_source();
|
||||
|
||||
// Stop the driver. The engine should call this when it's done
|
||||
// and there's nothing left to do.
|
||||
//
|
||||
void stop_driver();
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Creation and Destruction.
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
// Constructor.
|
||||
//
|
||||
// Most initialization is achieved by 'drv_xxx' functions, so
|
||||
// this constructor takes no arguments.
|
||||
//
|
||||
DrivenEngine();
|
||||
|
||||
// Destructor.
|
||||
//
|
||||
// It is necessary to delete all channels before deleting the
|
||||
// DrivenEngine. The destructor will verify that this has been done.
|
||||
//
|
||||
virtual ~DrivenEngine();
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
//
|
||||
// The following accessors are for use by PlayWrapper and ReplayWrapper.
|
||||
//
|
||||
// The PlayWrapper and ReplayWrapper use C stubs to access
|
||||
// the engine. The C stubs, in turn, call these C++ methods.
|
||||
//
|
||||
// The stubs for the getters are trivial, one-line stubs.
|
||||
//
|
||||
// The stubs for the mutators add logging.
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
void drv_get_listen_ports(uint32_t *nports, const uint32_t **ports) const;
|
||||
void drv_get_new_outgoing(uint32_t *nchids, const uint32_t **chids) const;
|
||||
const char *drv_get_target(uint32_t chid) const;
|
||||
bool drv_get_channel_released(uint32_t chid) const;
|
||||
void drv_get_outgoing(uint32_t chid, uint32_t *len, const char **data) const;
|
||||
bool drv_get_outgoing_empty(uint32_t chid) const;
|
||||
double drv_get_clock() const;
|
||||
bool drv_get_rescan_lua_source() const;
|
||||
bool drv_get_stop_driver() const;
|
||||
|
||||
void drv_initialize(uint32_t srcpklen, const char *srcpk, int argc, char **argv);
|
||||
void drv_clear_new_outgoing();
|
||||
void drv_sent_outgoing(uint32_t chid, uint32_t nbytes);
|
||||
void drv_recv_incoming(uint32_t chid, uint32_t nbytes, const char *bytes);
|
||||
void drv_notify_close(uint32_t chid, uint32_t len, const char *data);
|
||||
uint32_t drv_notify_accept(uint32_t port);
|
||||
void drv_invoke_event_update(double clock);
|
||||
void drv_set_lua_source(uint32_t srcpklen, const char *srcpk);
|
||||
|
||||
private:
|
||||
// Find a currently-unused channel ID. Channel IDs
|
||||
// are small integers that are reused.
|
||||
int find_unused_chid();
|
||||
|
||||
// Get the channel associated with the specified channel ID.
|
||||
Channel *get_chid(int chid) const;
|
||||
|
||||
private:
|
||||
SharedChannel channels_[DRV_MAX_CHAN];
|
||||
int next_unused_chid_;
|
||||
SharedChannel stdio_channel_;
|
||||
eng::vector<SharedChannel> accepted_channels_;
|
||||
eng::vector<uint32_t> new_outgoing_;
|
||||
util::LuaSourcePtr lua_source_;
|
||||
eng::vector<uint32_t> listen_ports_;
|
||||
bool rescan_lua_source_;
|
||||
double clock_;
|
||||
bool stop_driver_;
|
||||
friend class Channel;
|
||||
};
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
struct DrivenEngineReg {
|
||||
const char *name;
|
||||
DrivenEngineMaker maker;
|
||||
DrivenEngineReg *next;
|
||||
static DrivenEngineReg *All;
|
||||
DrivenEngineReg(const char *name, DrivenEngineMaker f);
|
||||
};
|
||||
|
||||
#define DrivenEngineDefine(name, cname) \
|
||||
UniqueDrivenEngine dengmake_##cname() { \
|
||||
return UniqueDrivenEngine(new cname); \
|
||||
} \
|
||||
DrivenEngineReg dengreg_##cname(name, dengmake_##cname);
|
||||
|
||||
struct DrivenEngineInitializerReg {
|
||||
static DrivenEngineInitializer func;
|
||||
DrivenEngineInitializerReg(DrivenEngineInitializer f);
|
||||
};
|
||||
|
||||
|
||||
#endif // DRIVENENGINE_HPP
|
||||
Reference in New Issue
Block a user