The C++ framework for developing highly scalable, high performance servers on Windows platforms.

Programming concepts used whilst writing this code

It's easier to understand how to use this code if you try and understand a little about the way it has been written...

The code is commented in such a way that Doxygen can produce documentation for it automatically. Doxygen produces a lot of documentation and it's not always that useful, but once you work out how to navigate the documentation it can be quite helpful. In addition we've tried to add pages that document key concepts used within the code and have linked the code into groups so that these concepts are easier to understand and so that you can navigate to the code concerned. Note: there are two builds of the documentation, one is the 'user' build which does not include embedded source code or private methods and the other is the 'developer' build which includes everything.

Static libraries
The code builds as static libraries, no effort has been put into making the code build as DLLs. Over the years we've found that if you want your production code to run with least surprises then the easiest way is to have the least dependencies possible. The more you depend on external things the more likely you'll get a support call at 4am because someone has changed one of the external things and it's not quite as compatible with the old version as you'd like. We don't like getting up at 4am to fix other people's problems and so we tend to build our reusable code into static libraries and then link them all together into one, single executable file where possible. See here and here for more details.

The Admin library
The Admin library is a collection of header files that all other libraries in the JetByte Tools suite use. Header files from the Admin library control things such as which warnings are disabled (Warnings.h) and which version of windows you're targetting the code for (see here for details of just how complex that simple thing could be...). See here for code that implements this concept.

Warnings level 4
All code builds at warning level 4. The Admin library contains a header file, Warnings.h that disables warnings that "don't matter", things like C4786 - "identifier was truncated to '255' characters in the debug information" and C4511 - "copy ctor could not be generated", etc. Some files may contain individual pragmas to disable specific warnings but we try and keep these to a minimum, things like C4355 - 'this' used as base member initialiser only. See here for code that implements this concept.

Warnings as errors
It's no good having your warning setting set to high if you're then allowed to ignore the messages. To that end we tell the compiler to treat warnings as errors. This means that you MUST act to remove warnings as they occur.

Unicode and Non-Unicode Builds
All of the code builds as either Unicode or Non-Unicode. This allows us to be maximally efficient on our primary platform (Windows NT/XP, etc) and yet still be able to use the code on older platforms (Windows 95) if required. See here for code that implements this concept.

Multi-Compiler support
All of the code builds with all versions of Visual Studio from Visual Studio .Net (2002) through to Visual Studio 2010. Up until release 5.2.3 we also supported Visual Studio 6. If you really need Visual Studio 6 support then talk to us about obtaining the earlier release but there really are lots of very good reasons for not using Visual Studio 6 anymore, see here for details of why you might be better off moving to a newer compiler. The structure of the project files is such that each compiler builds to its own output directory so that you can run multiple builds on multiple compilers at the same time.

x64 support
x64 and x86 builds are supported in Visual Studio 2005 and 2010.

Precompiled Headers
All of the code builds using precompiled headers for speed of compilation using #pragma hdrstop to detail where the precompiled header stops. There's also always a 'no precompiled header' build which is used to make sure that include file dependencies are 'correct' and kept to a minimum. In a nutshell, putting #pragma hdrstop in a source file that isn't compiled with /Yc or /Yu has no effect at all. If you have /Yu set for the file then hdrstop tells the compiler to throw away everything before the line on which hdrstop appears and insert the precompiled header instead. If /Yc is set for the file then hdrstop means save all of the compiled state for everything up to the line on which hdrstop appears as the precompiled header. The trick is using /Yc and /Yu without the optional header file name; just check the 'use' or 'create' radio button and leave the 'through header' edit box blank (or edit the dsp). See here for more details and here for code that implements this concept.

Build Configurations
All of the code has several build configurations as follows:

  • Debug
  • Release
  • Debug
  • Release
  • Debug No PreComp

The reasoning behind this is as follows. The libraries can be used to build code either as Unicode or non Unicode aware. It is potentially more efficient to work with Unicode builds on Windows NT platforms and potentially more efficient to work with non Unicode builds on Windows 9.x platforms as these builds are the 'native' builds for the platforms concerned and the 'non-native' builds often require data conversion for string data. In general it is sensible to only use the 'native' builds. So, for Windows NT based platforms (Win2k, XP, Vista, etc) you are only really interested in the Unicode builds.

The "Unicode Debug No PreComp" build is a build which does not use a precompiled header. It's useful when developing as it means that you don't trigger a complete rebuild of the whole system every time you change a single header file. It's slower to do a complete rebuild of the "no precomp" build than it is to do a complete rebuild of the builds that have precompiled header support.

In summary; in normal usage you would only ever build the Unicode Debug and Release builds. If you are making changes to the system then you may achieve faster "edit-compile-test" cycles if you use the "no precomp" build.

Debug logging and tracing
We use minimal debug tracing in the form of calls to JetByteTools::Core::Output and (even less often) JetByteTools::Core::OutputEx. If an application requires a trace log for support purposes then we implement one, probably using asynchronous file I/O to write the log using the JetByteTools::IO::CAsyncFileLog logger. A support log is NOT, in our opinion, the same as a debug trace, it's part of the application and should be designed with as much care as the rest of the application. Debug trace files tend to accumulate rubbish over time as trace statements are put in to help debug problems. See: here and here for more details.

Logging Log files and the pluggable logging system
Having explained our lack of enthusiasm for Debug tracing you might be surprised to find that there is a fairly complex pluggable logging system underneath our basic JetByteTools::Core::Output calls. This can be customised to provide a high performance trace log for supporting software that's been written with the framework. You can either use one of the many implementations of JetByteTools::ILogMessages or write your own and plug it in. Alternatively you can take a look at JetByteTools::CDebugTrace or JetByteTools::Win32::CDebugTrace (don't ask!) and write something similar to provide a separate system trace and debug log system, or use the log writers for other purposes...

These abstract base classes define Interfaces that concrete classes implement. They exist so that we can reduce coupling in the system. Since objects interact with each other through abstract interfaces and they don't care about the implementation of those interfaces we're free to plug in any object that implements the interface, such as a Mock Object, or alternative implementation. This allows us to evolve the system in a gradual manner; isolated objects can be replaced by new objects that implement the same interfaces without causing changes to ripple through the entire system due to unexpected coupling. See here and here for more details and here for code that implements this concept.

Protected destructors on abstract base classes
Many of the abstract base classes that we use as interfaces have protected destructors rather than the more usual public virtual destructors. This is because we never delete an instance of the object through a pointer to the interface concerned and by making the destructor protected we make such deletion impossible. See here for more details and here for code that implements this concept.

Most of this code uses exceptions for the reporting of errors. Most functions validate their input and throw exceptions on bad input. Most fuctions throw exceptions if there are errors that they can't handle. There are a surprisingly small number of exception classes used, this is by design. Usually there isn't a great deal that the calling code will want to do that depends on why the code failed, if there's that kind of choice available then the code that's being called usually exposes that choice. This means that if an exception occurs it's often not caught by anything except the final exception handler in the system, the one that lives at process or thread boundaries and, generally, pretty much all that will do is log the failure. This is all by design and all works well for writing reliable code. See here, here, here and here for more details and here for code that implements this concept.

Exception Specifications
Whilst exception specifications in C++ can, at first glance, seem like a good idea, they're not. The problem is that, unlike Java, the exception specification is not enforced at compile time, they're just a runtime thing. If an exception that isn't in the exception specification is thrown by a function then unexpected() may (or may not if you're Visual C++) be called. As such we view exception specifications as of limited use in C++. However: we do use a 'no throw' (throw()) exception specification to indicate that a function does not throw exceptions. We rarely use a 'no throw' specification to indicate that YOU should not throw exceptions, simply because we don't trust you ;) See here for code that implements this concept.

We don't use templates very often (apart from the STL, of course!). This is deliberate. We feel that often templates are overused and often the code using them can be more complex than need be. And, we're constrained by the fact that we need to support old compilers that often don't allow us to use templates when we'd like to... We do use templates where we can and where we feel that they add value to the construction and maintenance of the code. See here for code that implements this concept.

Many classes support a monitoring interface. This allows us to see what's going on and, perhaps, hook these classes up to performance counters that can be viewed by perfmon, etc. See here for code that implements this concept.

Patterns with a small 'p'
Patterns are a great way of being able to communicate clearly about a programming concept, but some people get a little too hung up on them. We use patterns sparingly and, hopefully, only where appropriate... The most common patterns in use in our code are listed below:
  • The Handle/Body idiom
    We use the Handle/Body idiom to provide compilation 'firewalling' that protects the main body of code from becoming unintentionally coupled to code that we're integrating with. By breaking an object into a Handle and a Body and only allowing the Body to have knowledge of the code that we're integrating with we make sure that our main body of code never includes an integration header file directly or uses any types from the integrated library. The advantages of this approach are that, theoretically, the implementation of the Body can change without affecting our main code base. See here and here for more details and here for code that implements this concept.
  • Parameterise From Above
    Parameterise From Above is an informal Pattern that was first used by Kevlin Henney. It describes a system of explicitly 'plugging together' objects to provide services rather than allowing objects to 'reach out' and grab the services that they require. In essence objects are configured 'from above' by being given the other objects that they require to do their work. This is pretty much the opposite of a Singleton. See here for more details and here for code that implements this concept.
  • RAII - Resource Acquisition Is Initialization
    This simple idiom relies on the lifetime of an object to control the lifetime of an associated resource. In essence, the resource is released when the object is destroyed. This gives us simple, scope-based usage of resources that is safe in the presence of exceptions and multiple function exit points. See here for more details and here for code that implements this concept.
  • Smart Pointer
    A smart pointer is a generic term for an object that helps with RAII style 'scope based' designs. The 'smart pointer' usually takes ownership of a 'bare' pointer and takes responsibility for destroying the 'bare' pointer when the 'smart pointer' goes out of scope. See here for more details and here for code that implements this concept.
  • Abstract Factory Pattern
    This simple idiom provides a way to centralise the creation of a particular type of object so that to obtain an instance of the class that you want must ask the factory to create it for you. This allows for different factories to provide slightly different variations on the object that you get. See here for more details and here for code that implements this concept.
  • Null Objects
    These objects implement various Interfaces in a way that does nothing. They exist so that we can use the Null Object Pattern see here, rather than allowing optionality via pointers that can be null we use references and use a 'null object' when we don't require the functionality. This simplifies the code as it removes the need to test for null and switch depending on a pointer's state; we always call the method and if the object is a null object the call just does nothing. See here for code that implements this concept.
Lock Free Algorithms
We've toyed with using lock free data structures to improve the performance of the socket and buffer allocators but our initial performance profiling didn't show that great an improvement so we haven't done a great deal of work in this direction... See here for more details and here for code that implements this concept.

In general we do a lot of refactoring to keep the code base clean and to ensure that changes are made in such a way that the design improves over time rather than rots. The refactoring is supported by our unit testing.

Unit Tests
Where possible we've tried to make all code in the system testable. The idea is that rather than testing the system as a whole, you can test it in pieces. This can make debugging easier as you don't have to manipulate the entire system to get it into the state you want, you can just set up a test that gets the piece of code that you need to test into the correct state... Of course, testing requires that you write more code, and writing code takes time, so we've tried to focus our tests on the places that give us the best 'bang for the buck'. This means that coverage isn't as good as we'd like it to be, but the important test, the first test, is there, in most cases. Moving forward it would be good if each time a bug is located a test is first constructed that duplicates the bug and then the bug is fixed so the test passes. Working in this way quickly builds a useful set of regression tests for the code.
You might be able to see here for code that implements this concept.

Note: just because the code that you have in front of you doesn't seem to have any unit tests included it doesn't mean that they don't exist. The normal licensed version of the code doesn't ship with our unit tests. If you'd like to license them, get in touch.

Mock Objects
These objects implement various Interfaces in a way that's useful for testing. Rather than using a real implementation you use a Mock which allows you to instrument and manipulate the object in ways that are useful for testing. See here for more details. You might be able to see here for code that implements this concept.

More information

Generated on Sun Sep 12 19:06:45 2021 for The Server Framework - v7.4 by doxygen 1.5.3