Guildhall Coding Conventions

The coding conventions I use for developing Guildhall are strongly influenced by practices developed by the Pink (Apple) and Taligent engineering teams. You can read about those practices if you can find a copy of Taligent's Guide To Designing Programs: Well-Mannered Object-Oriented Design in C++, edited by D. Goldsmith, published by Addison-Wesley in 1994. That said, I'm quite sure the contributors would be horrified by the manner in which I've subverted some of those practices, but that keeps life exciting, right? Since you aren't likely to find a copy of that work, and since I've politely given its authors credit, I'll just get on with telling you how I do things.

General Guidelines

C++ is based, of course, on the C programming language, and it inherits some generally problematic conventions. So here are some syntax elements that C allows that I explicitly avoid.

  • goto statements
  • using the return statement from anywhere but the end of a function
  • the comma operator
  • using automatic compare to 0 as in "if (intVariable)" or "if (pointerVariable)". I explicitly compare them to 0 or NIL
  • multiple variables defined by a single type (as in "int x,y,z;" or "void *a, **b;). Every variable gets its own line.
  • using macros or defines (with certain well-behaved exceptions, such as controlling the visibilty of code blocks and include files or the no-op macros used to tag header classes and methods for Codex)

And then there's C++, which also has some syntax elements that I religiously avoid.

  • virtual base classes
  • multiple inheritance from more than one constructible class
  • inheritance trees with more than one instance of a parent class
  • anything that looks overly complicated and prone to mistakes

So, what about conventions I do use? Here are a few.

  • All constructible classes (classes with a public destructor and at least one public constructor) start with "T". It doesn't stand for Taligent, it might stand for "Type", it might be a MacApp convention. I really don't know. I've thought about changing them all to start with "C" or "Gh" but I haven't yet. So for now they all start with "T" until further notice.
  • All abstract classes start with "A". An abstract class will not have a publicly available constructor and must have at least one protected constructor for subclasses to use. It must have a public virtual destructor. It must be designed to be subclassed from, and that doesn't mean making every function virtual. In fact, it should only offer virtual methods as part of its design, not as a hedge against future wish lists. In theory. Don't hold me to this one. I'm sure there are classes I haven't visited in years that violate this rule.
  • A mix-in class is an abstract class that augments the functionality of a constructible class. All mix-in classes start with an "M". They may provide discrete methods and storage, not intended to be overridden, and generally they provide a set of virtual functions intended to be overridden. These usually come with benign behavior if not overridden. In some cases they will throw exceptions if called. I could have used the virtual base class mechanism instead but I feel it causes more issues than it solves. The exception model has worked well.
  • Composite names capitalize each subword and avoid underscores, as in MySpecialFunction or YourNiftyAlgorithm. I use an exception to this rule for helper functions, which I'll describe in a bit.
  • Class member variables start with 'f'. I think that's for "field" but I'm not sure. Constants start with 'k', and I'm pretty sure that's both a Taligent and MacApp convention, and Apple uses it, too. Taligent also used 'k' to start enums, and I abandoned that practice in favor of starting them with 'e', and starting the enum type declaration with a capital 'E'. Global variables start with 'g', and Taligent used a capital 'G' to denote global type names. I have a few scalar types that follow that convention but, as you will see below, I don't use it consistently. Local variables should start with a lower case letter. It's okay to start a local variable with 'f' so long as the next character is not capitalized.
  • Finally, I have a number of helper functions that are basically inlines. These helpers sometimes map onto global functions and sometimes map onto static functions of classes that mostly contain static members. All of the standard math classes are available in this form, for example fdual2sin or fquad2cos. To avoid ambiguity, I put the numeric type in the helper name and the desired calculation as the last part of the name. Some helpers map onto classes. For example, ulong2str maps from an unsigned 32 bit integer to an instance of TString that contains the representation of the number supplied.

Scalars

One aspect of cross-platform development that I find frustrating is the platform differences for scalar types. Not only can they change from one host to another, they can change when the host OS makes changes. Apple did this a bunch of years back in its transition to 64-bit architectures. From my perspective it's useless. The memory organization isn't changing, and while some of these modified types are dictated by changes in evolving C/C++ language specifications, they're detrimental to maintaining working code. With that in mind, I opted for the following simplified scheme which I overlay on top of what's provided by any host system I support.


Scalar

Type

Scalar

Type

schar

signed 8-bit integer

uchar

unsigned 8-bit integer

sword

signed 16-bit integer

uword

unsigned 16-bit integer

slong

signed 32-bit integer

ulong

unsigned 32-bit integer

shuge

signed 64-bit integer

uhuge

unsigned 64-bit integer

svast

reserved signed 128-bit integer

uvast

reserved unsigned 128-bit integer

freal

32-bit floating point

fdual

64-bit floating point

fquad

128-bit floating point

focho*

reserved 256-bit floating point

coord

geometry coordinates (fdual)

pctge

percentages, 0.00 to 1.00 (fdual)

ucode

32-bit unicode char

nbool

host boolean (stored as uchar)

nchar

host 8-bit character

nwide

reserved host 16-bit character

*or maybe facht or focto or foctp or foctu - actually I don't like any of them


In addition to these, I have a few pseudo scalar types that come into play when handling the four types of enum declarations, meaning having some signed items versus having exclusively unsigned items, and fitting within a 32 bit integer versus requiring a 64 bit integer.


Pseudo Type

Enum Constraint

Pseudo Type

Enum Constraint

SEnum

up to 32-bit signed values

UEnum

up to 32-bit unsigned values

SEpic

up to 64-bit signed values

UEpic

up to 64-bit unsigned values

the capitalization shown is intentional as it reflects how Codex references these pseudo types


Memory Management

A mostly static class called THeap, declared in the Primitives section, is responsible for tracking all memory management in Guildhall. It does this by virtue of the convention that every class that can be allocated by new must override the new and delete operators in the following fashion.


class TType {
    public :
        TType(uchar* staticType);
    private :
        uchar fTypeData[24];
}

static TType TClass::GetStaticType()
{
    static TType& staticType("TClass");
    return staticType;
}

void* TClass::operator new (
    GAllocation size )
{
    static THeap& heap = gHeap = THeap::GetAllocator();
    return gHeap.CreatePointer(TClass::GetStaticType(),size,kFalse);
}

void TClass::operator delete (
    void* pointer,
    GAllocation size )
{
    static THeap& heap = gHeap = THeap::GetAllocator();
    gHeap.DeletePointer(TClass::GetStaticType(),pointer,size,kFalse);
}


I adopted this scheme before RTTI was settled, and it has served me quite well. The THeap code tracks a lot of information about the pointers created as Guildhall runs, and automatically reports memory leaks. The code guards against any pointers being allocated before static initialization is complete and after static termination has begun. It writes type IDs and the requested pointer length into a header tacked onto the front of every pointer and puts scribble guards both before and after the pointer data. If need be the class can also enable a great deal of instrumentation to help track down difficult pointer issues, for example by checking every pointer for scribbling. But such problems have become quite rare. In fact, if I do get a random crash the first thing I check is to see whether the program stack has been overrun somehow. It seems to me that the runtime Guildhall most often runs upon ought to be quite a bit more graceful about that sort of thing, but it isn't.

One other comment about memory management, and this is solely a matter of personal preference. I happen to despise non-deterministic coding architectures, and I explicitly refer to garbage collection. I'm not referring to what the memory system has to do to organize and recover deallocated pointers, more the process of ignoring unreferenced pointers, assuming the system will come along eventually and clean things up. I try to carefully deallocate every pointer I create, and I have plenty of tools in place to assist with that. Moreover, I will zero out pointers in the destructors of classes, even if those pointers were just aliases. If there's a dangling reference out there I want it to crash early and often so I can find it and get rid of it. That was another Taligent design principle, getting bugs to crash early and often, and it's a fine one. In a similar way, the ordering of constructor calls during static initialization is essentially non-deterministic. I solve this by causing all heap allocations to fail if static initialization is not complete. I have tools that can defer heap-based initialization until after the heap is ready.

Persistent Classes

To fit within the Guildhall architecture, classes which encapsulate persistent data must implement a suite of standard methods. The following sample class provides an overview.


class TClass {
    // constructors and destructors
    public :
                            ~TClass();  // destructor may be virtual
                            TClass();
                            TClass(const TClass& other);
                            TClass( ... );  // class specific constructors
    // Static methods
    public :
        static TType        GetStaticType();
        static ...          ... // class specific static methods
    // Member functions
    public :
               ...          ... // class specific member methods
    // Standard methods
    public :
               void*        operator new(GAllocation size);
               void         operator delete(void*,GAllocation size);
               TClass&      operator=(const TClass& other);
               TClass*      CopySelf() const;
               AStream&     operator>>=(AStream& output) const;
               AStream&     operator<<=(AStream& input);
               void         Export(ATextFile& output,const TString& id) const;
               void         Import(ATextFile& input,const TString& id);
               nbool        operator==(const TClass& other) const;
               nbool        operator!=(const TClass& other) const {
                                return !operator==(other); }
               void         Compare(AConsole*,const TClass* a,const TClass* b,const TString& message);
               void         Compare(AConsole*,const TClass& a,const TClass& b,const TString& message);
               ...          ... // class specific operators
    // Member variables
    private :
               ...          ... // class specific member variables
};


GetStaticType is used for a number of purposes, but its role in memory management is certainly the most prominent. CopySelf is specifically not a virtual function. There is an analogous Clone() function for polymorphic classes. CopySelf should never be implemented in Abstract or Mix-in classes.

AStream is the abstract class that stands in for several persistent stores, including binary host files, Guildhall archive files, and the special memory-based object TMemoryStream. The paired operators >>= and <<= write and read, respectively, a binary representation of TClass. Support routines enable the streaming of scalars, including enums. For class based members, the same operators implemented in the member classes provide streaming support. Polymorphic members have additional support, not described here, that allow such members to be properly stored and retrieved.

ATextFile is the abstract class that stands in for text based stores, including host text files and Guildhall archive text files. The text output for Export and Import is similar to XML, or at least as close as I could get it—I didn't study the XML conventions. If it's important I could probably make it conform. It hasn't yet been important. The purpose of these methods is a sort of fail safe against insurmountable upgrade issues. If I can at least get the data out in XML form I can write something to reconstitute it for a different system. And although I wrote the methods as Export and Import, I really wish I had used the operators for >> and <<. It would have had a nice symmetry. Maybe someday I'll convert to those operators.

Finally, the Compare methods are solely for debugging. The first one typically uses some standard Guildhall calls to check the pointers for NIL and call the second one if it has two non-NIL pointers. The second one should iterate across each pertinent member variable, relying upon helper classes for scalars and upon embedded Compare calls for class-based members.

Templates

While I'm sure this will horrify C++ afficionados, I do not use any C++ standard library templates. Instead I use home-grown templates. Following the Taligent convention, I almost always use C++ templates as a thin layer over constructible helper classes. The constructible helper class does the work, usually by manipulating pointers, and the template handles type conversion. I have a few such constructible classes that have been working well for a long time.

By convention, my template declarations start with X. The most prominent helper classes are TPointerArray, which manages an embedded array of void*, and TNamedPointerArray, which subclasses from TPointerArray and adds the ability to index its void* pointers with strings (as TString). XPointerArray and XNamedPointerArray are, respectively, built upon those two classes, converting the managed pointers from type void* to TClass*. So XPointerArray and XNamedPointer would each manage an array of pointers to TClass, but the latter would also allow indexing by strings.


typedef XPointerArray TClassArray;

typedef XNamedPointerArray TNamedClassArray;


Other Conventions

Other conventions will be discussed as necessary to explain each part of the Guildhall architecture.

 

Copyright © 1981-2021 Arthur W Cabral. All Rights Reserved. All referenced trademarks are the property of their respective owners. This site does not use cookies. This site does not collect visitor information. The ISP hosting this site collects statistics regarding visitors to this site as part of the normal operation of the website. We do not currently examine those statistics. If that changes, this notice will change.