GNE API Major Changes
Since the last public release of GNE (0.55) I have decided to make significant
changes to the API.
I choice this for a few reasons:
- I designed the GNE API over two years ago in May of 2001. Since then
I've learned a lot of better ways to design things. I know this is always
an ongoing battle since you can always do better, but I just had a 6
month break of working on GNE, and the wacky memory allocation stuff
is getting to me.
- Requiring the user to use
delete this
; without signficant
work is just plain outright horrible. I wanted to make memory allocation
very simple and let the library do the work. I know this may annoy some
but GNE is more geared toward freeware / independent developers whose
time is much more valuable than having absolute control.
- I figure that at this time, hardly anyone uses GNE, so if I wanted
to redesign the API, I'd better do it now before people start using
it (if ever... hopefully ;).
- I'm changing more of the way you do things rather the way your programs
are designed around GNE. In most cases I expect people removing complicated
code or only adding simple functionality that will make their program
much more robust. I'm not changing any of the underlying concepts for
GNE. I've tried to put my attention to only the largest design oversight
of GNE -- and that is in the threading and memory allocation code.
To tell the truth, the only real huge change in the existing GNE code
is the move to reference-counted pointers almost exclusively throughout
the code. While it sounds simply stated it actually has a lot of effect
on the calling code such that work is required to port to the new GNE
-- not much -- but still enough I'd like to call it "major API change."
I'm thus writing this document which will be, more or less, your porting
guide. I hope to document the changes here as best as my time allows.
It is very possible I missed something out here, so please tell me if
you find any mistakes in this document.
This is NOT a complete change guide but instead a guide to the major
changes that require changes to your code. See the Changes file for added
functionality and other changes.
Smart Pointers
In this new version of GNE I decided to make a move to using smart
pointers. The current implementation that I have chosen is Boost's
shared_ptr, which is a non-intrusive reference counted smart pointer. In
the GNE API, the smart pointers manifest themselves as the GNE::SmartPtr
class. Yes, I did find out later the popular Loki library also has a
class "SmartPtr" but this is what namespaces are for.
Because SmartPtr is a reference counted pointer, that means when the
last SmartPtr pointing to an object dissapears, the object is
automatically deleted. Reference counting in this sense appears exactly
like garbage collection except for one major flaw -- if an object directly
or indirectly (through other classes) points to itself (this is called a
cycle), then the object will never be deleted, causing a memory leak. The
way to break cycles with boost::shared_ptr is to use boost::weak_ptr,
which in GNE manifests itself as GNE::WeakPtr. Weak pointers "become
empty" when no strong pointers point to that object -- in other words weak
pointers to not keep the object from being destroyed.
Unfortunately this makes the generator-listener relationship difficult.
To keep the same design as GNE 0.55 of registering listeners and then
being able to "forget" about them, I resolve cycles by keeping a SmartPtr
to the listener and manually resetting it when the generator generates its
last event. Because Connection and Timer objects are automatically shut
down when GNE is shutdown on program exit, no memory leaks will result.
This means you can safely retain SmartPtr to GNE's objects in your
listeners. I wanted to make the issue of cycles as transparent as
possible to the end-developer so I place all of the burden of handling
possible cycles with GNE.
Reference counting has a few key advantages over true garbage
collection:
- Objects are destroyed the instant they are not being used, so object
destructors can be used to free resources immediately rather than
lingering around until the next collection which may not come for
minutes, hours, or days depending on the memory environment.
- Determining when an object ready to be deleted is much easier and
much more efficent in a reference-counted system as only a simple
integer decrement and test is required. Likewise copying a reference
counted pointed is efficent requiring only an increment operation and
copying of typically only an additional pointer to the count.
GNE enforces the use of smart pointers to its objects by making the
constructors private and providing public static "create" methods that
construct a new object and return a SmartPtr to it. By forcing objects to
be referenced by a smart pointer from the very start, it prevents any
accidental leaks, espically unexpected ones due to exceptions.
Interface Changes
Thread class:
- You MUST set the member "thisThread" immediately after creating
your specific instance of Thread. You will want to write a static "create"
method (see examples for details).
ServerConnectionListener class:
- You need to create a static "create" method for your subclass,
as in other instances.
- The previous way to shut down a ServerConnectionListener was simply
to destruct it, but that is not possible anymore since object deletion
is automatic now. There is now an explicit close command.
- getLocalAddress is now safe to call at any time. Rather than undefined
behaviour, it will return an invalid Address object if it is not listening.
Address class:
- The default constructor now returns an invalid address. This is now
explicit. Before it returned the address "0.0.0.0:0" and its
validity was unclear by the documentation.
Connection and ConnectionEventGenerator class:
- The setListener and reg/unreg listener registration methods no longer
block to wait for the current listener to be unset. This design scheme
was found to very often lead to deadlock, since a mutex had to be left
acquired during the event call, and we can make no guarantees about
what mutexes it might lock or calls it might make. The only main reason
I found that would require blocking is if you wanted to delete the listener,
but because listeners are now managed by SmartPtr, this should be OK.
We have found that the better design pattern is to allow method to be
called on objects after they have been "shutdown" -- since
a connection can become disconnected at any time it is impossible to
determine if the connection is active to send a packet, thus when a
connection is disconnected, the written packet is ignored (rather than
some sort of crash or undefined behavior).
GNE Initialization and Shutdown:
- When GNE is shutdown either explictly through the shutDown method
or implicitly by main ending, it will request a disconnect on all connections,
stop all timers, and call shutDown on all threads. It will then wait
some time (default 10 seconds) for everything to shut down. If the process
takes too long GNE will forcefully shutdown anyway. The reason for this
is because it was hard to guarantee things were finished closing/disconnecting
so it was possible even for a well-written and well-behaved program
to get events generated after GNE closed, meaning that most of the time
the program crashed on exit since the events referenced non-existant
objects.
Packet class
- The makeClone and the static create methods are no longer needed for
Packet. See the PacketParser namespace changes for more details about
registering packets with default behavior using new/delete.
CustomPacket class
- The CustomPacket class interface has changed somewhat due to the new
Buffer class. It is a lot more robust now and handles a larger variety
of situtations for ease of use. It is also safer to use.
Deprecated Classes
RawPacket class
- The RawPacket class wasn't very safe, and did not provide adequate
buffer overflow/underflow protection. Now it does so completely. Also
the RawPacket name was confusing to newcomers because the RawPacket
class ended in the word "Packet" when it was not derived from
GNE::Packet. The functionality the old RawPacket class served is now
filled by the new Buffer class.
Deprecated Methods
PacketParser Namespace:
registerPacket( id, createFunc ) - this function has
been deprecated, because just specifying a creation function was not enough.
GNE assumed that the create function created a packet with the "new"
operator, and that deleting it with the "delete" operator was
acceptable. This has two drawbacks, the first being that the user couldn't
use their own memory allocation scheme for packets, and the second being
that the library was not able to be in a DLL.
While work on a DLL version of GNE has not started, this would have been
a major flaw to overcome in that process, so switching makes GNE one step
closer.
Now the user is required to supply a creation function, a clone function
(replaces what "makeClone" did before), and a destroy function.
GNE will call the destroy function rather than "delete" when
it destroys a packet now.
However, specifying three functions now does not mean there is more work
on the user. On the contary, templated default functions are now provided
for creation using the new operator, cloning using the copy constructor,
and deletion using the delete operator. Because of the way templates work,
this still means the user's code, not GNE's code will be calling the new/delete
operator of the user's C++ run-time and not GNE's.
So for users who are not interested in writing their own packet allocator
(probably almost all of you), the implementation has now actually become
a lot simpler. You no longer need to define a static "create"
function or overload a virtual makeClone function, if you use the defaultRegisterPacket
function. You do need to defined a "static const int ID" field
for your class. Then once you've done that, you register it like so:
class MyPacket : public Packet {
public:
MyPacket() : Packet( ID ) {}
MyPacket( const MyPacket& o ) : Packet( o ) {}
virtual ~MyPacket() {}
static const int ID;
//readPacket, writePacket, getSize methods...
};
const int MyPacket::ID = MIN_USER_ID;
//Then in main method:
GNE::PacketParser::defaultRegisterPacket<MyPacket>();
And that's all there is to it... The previous API did not require the
copy ctor to be properly defined, but now it needs to. It is good practice
to do so anyway.
Thread class:
- detach - not needed anymore because of smart pointers
Connection class:
- getListener - The semantics of method have changed
to pretty much make it useless. It's probably dangerous to provide this,
so I'm considering removing it. If you have a strong case to leave it
in, please tell me.
ClientConnection class:
- The join method is no longer supported. The waitForConnect
method is now the way to wait for a connection to complete. The Thread
interface probably should never have been exposed.
Console Namespace:
- initConsole taking the atexit pointer is deprecated.
There is no need for it anymore, since the Console is now shutdown when
GNE is shutdown. This also fixed a bug in the process of removing a
redundant parameter. Calling "initConsole( atexit )" is the
same as "initConsole( true )" in the new scheme. The new boolean
parameter specifies if you want the screen cleared when the program
exits.
CustomPacket class:
- reset - This method is no longer needed. You can
use getBuffer().clear() to achieve the same effect.
- getMaxUserDataSize - This method is no longer needed.
You can use getBuffer().getCapacity() to achieve the same effect.
All content on this web site is copyright © 1998-2020 by Jason Winnebeck, unless otherwise noted.