GuidesCoding Tidbits and Style That Saved My Butt

Coding Tidbits and Style That Saved My Butt

When you pick up a game programming book, the last you likely want to do is read about programming style and specific coding techniques. After all, you probably want to dive right in and learn how to code up 3-D graphics or sound. Before getting to that, I’d rather show you some useful things you’ll use throughout your entire code base. I’ll also show you some things to avoid in your code, much of which is setup for working with other programmers. Your code should communicate clearly to other programmers at every opportunity. Something I’ve learned over the years is that the distance between exuberance and experience is paved with mistakes. This sometimes makes older programmers a little less likely to embrace new things.

In the first edition of this book, this chapter was called “dumb stuff all game programmers should know.” It turned out that this stuff wasn’t so dumb, or obvious. My goal in this chapter is to set the foundation for the coding techniques that I’ll be presenting throughout this book. I’ve developed this style over the years watching really smart people, and the techniques have worked for me so I’ve keep them around.

But as you read this chapter, keep in mind that when it comes to programming style, programmers have different ways of doing things. For example, the techniques you use to program games on a PC platform, where you have more robust tools and plenty of memory to work with, might be different than the techniques you’ll use to program on a platform such as the PS2. Using C or C++ also makes a huge difference in style. In other words, not every problem has a single solution and no single style fits all situations. Also every programmer, and programming team is different. They’ll sometimes never agree on things and I don’t expect you’ll agree with everything I present in this chapter, nor this book.

Let me put it this way—if you find something you really hate, it means you have opinions different than mine, and you’ve formed those opinions through first hand pain and suffering. That’s great; it means you’re a programmer, and you and I can debate endlessly on the web about the best way to do things.

We’ll start by looking at design practices that you should consider when writing a game and then we’ll move on and look at specific programming techniques such as working with pointers, memory management, how to avoid memory leaks, and other goodies. In the last part of the chapter I’ll provide you with a few coding tools taken from my own personal toolbox that I usually employ to develop games for companies such as Eidos, Microsoft, and Origin.

Smart Design Practices

Isaac Asimov’s Foundation series invented an interesting discipline called psycho-history, a social science that could predict societal trends and macro events with great certainty. Each historian in the story was required to contribute new formulas and extend the science. As a programmer, your job is similar. Every new module or class that you create gives you the opportunity to extend the abilities and usefulness of the code base. But to do this effectively, you must learn how to think ahead and design code with the goal of keeping it in use for many projects and many years.

Designing good code in an object-oriented language can be more difficult than in a procedural language like C or PASCAL. Why? The power and flexibility of an object-oriented language like C++, for example, allows you to create extremely complicated systems that look quite simple. This is both good and bad. In other words, it’s easy to get yourself into trouble without realizing it. A good example of this is the C++ constructor. Some programmers create code in a constructor that can fail. Maybe they tried to read data from an initialization file and the file doesn’t exist. A failed constructor doesn’t return any kind of error code, so the badly constructed object still exists and might get used. Another example is the misuse of virtual functions. A naïve programmer might make every method in a class virtual, thinking that future expandability for everything is good. Well, he’d be wrong. A well thought through design is more important than blind application of object oriented programming constructs.

You can make your work much more efficient by improving how you design your software. With a few keystrokes you can create interesting adaptations of existing systems. There’s nothing like having such command and control over a body of code. It makes you more artist than programmer.

A different programmer might view your masterpiece entirely differently, however. For example, intricate relationships inside a class hierarchy could be difficult or impossible to understand without your personal guidance. Documentation, usually written in haste, is almost always inadequate or even misleading.

To help you avoid some of the common design practice pitfalls I’m going to spend some time in this chapter up-front discussing how you can:

  • Avoid hidden code that performs nontrivial operations
  • Keep your class hierarchies as flat as possible
  • Be aware of the difference between inheritance and containment
  • Avoid abusing virtual functions
  • Use interface classes and factories
  • Use streams in addition to constructors to initialize objects

Avoiding Hidden Code and Nontrivial Operations

Copy constructors, operator overloads, and destructors are all party to the “nasty” hidden code problem which plague game developers. This kind of code can cause you a lot of problems when you least expect them. The best example is a destructor because you never actually call it explicitly; it is called when the memory for an object is being deallocated or the object goes out of scope. If you do something really crazy in a destructor, like attach to a remote computer and download a few megabytes of MP3 files, you’re teammates are going have you drawn and quartered.

My advice is that you should try to avoid copy constructors and operator overloads that perform non-trivial operations. If something looks simple, it should be simple and not something deceptive. For example, most programmers would assume that if they encountered some code that contained a simple equals sign or multiplication symbol that the it would not invoke a complicated formula such as a Taylor series. They would assume that the code under the hood would be as straightforward as it looked—a basic assignment or calculation between similar data types such as—floats or t doubles.

Game programmers love playing with neat technology, and sometimes their sense of elegance drives them to push non-trivial algorithms and calculations into C++ constructs such as copy constructors or overloaded operators. They like it because the high level code performs complicated actions in a few lines of code, and on the surface it seems like the right design choice. Don’t be fooled.

Any operation with some meat to it should be called explicitly. This might annoy your sense of cleanliness if you are the kind of programmer that likes to use C++ constructs at each and every opportunity. Of course there are exceptions. One is when every operation on a particular class is comparatively expensive, such as a 4×4 matrix class. Overloaded operators are perfectly fine for classes like this because the clarity of the resulting code is especially important and useful.

Sometimes you want to go a step further and make copy constructors and assignment operators private. This keeps programmers from assuming the object can be duplicated in the system. A good example of this is an object in your resource cache, such as an ambient sound track that could be tens of megabytes. You clearly want to disable making blind copies of this thing because an unwary programmer might believe all he’s doing is copying a tiny sound file.

A recurring theme I’ll present throughout this book is that you should always try to avoid surprises. Most programmers don’t like surprises because most surprises are bad ones. Don’t add to the problem by tucking some crazy piece of code away in a destructor or similar mechanism.

Class Hierarchies: Keep Them Flat

One of the most common mistakes game programmers make is that they either over-design or under-design their classes and class hierarchies. Getting your class structure well designed fo your particular needs takes some real practice. Unfortunately, most of my experience came the hard way through trial and error. But you can learn from some of my mistakes and unique techniques that I’ve picked up along the way.

Tales from the Pixel Mines

My first project at Origin developed with C++ was Ultima VII. This project turned out to be a poster child for insane C++. I was so impressed by the power of constructors, virtual functions, inheritance, and everything else that once I got the basics down I went nuts and made sure to use at least three C++ constructs on every line of code. What a horrible mistake! Some Ultima VII classes were seven or eight levels of inheritance deep. Some classes added only one data member to the parent.our impotent attempt at extending base classes.

We created so many classes in Ultima VII that we ran out of good names to use. The compiler was so taxed by the end of the project that we couldn’t add any more variables to the namespace. We used global variables to store more than one piece of data by encoding it in the high and low words rather than creating two new variables. By the end of the project I was terrified of adding any new code, because the compiler would fail to build the project having hit some crazy limitation.

On the opposite end of the spectrum, a common problem found in C++ programs is the Blob class, as described in the excellent book Antipatterns, by Brown, et. al. This is a class that has a little bit of everything in it, and is a product of the programmer’s reluctance to make new, tightly focused classes. In the source code that accompanies my book, the GameCodeApp class is probably the one that comes closest to this, but if you study it a bit you can find some easy ways to factor it.

When I was working on Ultima VII we actually had a class called KitchenSink and sure enough it had a little bit of everything. I’ll admit to creating such a class on one of the Microsoft Casino projects that I worked on that would have made intelligent programmers sick to their stomachs. My class was supposed to encapsulate the data and methods of a screen, but it ended up looking a little like MFC’s Cwnd class. It was huge, unwieldy, and simply threw everything into one gigantic bucket of semi colons and braces.

Professionally I like to use a flat class hierarchy. I’ve also used this approach for the source code for this book. Whenever possible, it begins with an interface class and has at most two or three levels of inheritance. This class design is usually much easier to work with and understand. Any change in the base class propagates to a smaller number of child classes, and the entire architecture is something normal humans can follow.

Try to learn from my mistakes: Good class architecture is not like a Swiss Army Knife; it should be more like a well balanced throwing knife.

Inheritance vs. Containment

Game programmers love to debate the topics of inheritance and containment. Inheritance is used when an object is evolved from another object, or when a child object is a version of the parent object. Containment is used when an object is composed of multiple discrete components, or when an aggregate object has a version of the contained object.

A good example of this relationship is found in user interface code. A screen class might have the methods and data to contain multiple controls such as buttons or check boxes. The classes that implement buttons and check boxes probably inherit from a base control class.

When you make a choice about inheritance or containment your goal is to communicate the right message to other programmers. The resulting assembly code is almost exactly the same, barring the oddities of virtual function tables. This means the CPU doesn’t give a damn if you inherit or contain. Your fellow programmers will care, so try to be careful and clear.

Virtual Functions Gone Bad

Virtual functions are powerful creatures that are often abused. Programmers often create virtual functions when they don’t need them or they create long chains of overloaded virtual functions that make it difficult to maintain base classes. I did this for a while when I first learned how to program with C++.

Take a look at MFC’s class hierarchy. Most of the classes in the hierarchy contain virtual functions which are overloaded by inherited classes, or by new classes created by application programmers. Imagine for a moment the massive effort involved if some assumptions at the top of the hierarchy were changed. This isn’t a problem for MFC because it’s a stable code base, but your game code isn’t a stable code base. Not yet.

An insidious bug is often one that is created innocently by a programmer mucking around in a base class. A seemingly benign change to a virtual function can have unexpected results. Some programmers might count on the oddities of the behavior of the base class that, if they were fixed, will actually break any child classes. Maybe one of these days someone will write an IDE that graphically shows the code that will be affected by any change to a virtual function. Without this aid, any programmer changing a base class must learn (the hard way) for themselves what hell they are about to unleash. One of the best examples of this is changing the parameter list of a virtual function. If you’re unlucky enough to change only an inherited class and not the base class, the compiler won’t bother to warn you at all; it will simply break the virtual chain and you’ll have a brand new virtual function. It won’t ever be called by anything, of course.

Best Practice

If you ever change the nature of anything that is currently in wide use, virtual functions included, I suggest you actually change its name. The compiler will find each and every use of the code and you’ll be forced to look at how the original was put to use. It’s up to you if you want to keep the new name. I suggest you do, even if it means changing every source file.

From one point of view, a programmer overloads a virtual function because the child class has more processing to accomplish in the same “chain of thought.” This concept is incredibly useful and I’ve used it for nearly ten years. It’s funny that I never thought how wrong it can be.

An overloaded virtual function changes the behavior of an object, and gains control over whether to invoke the original behavior. If the new object doesn’t invoke the original function at all, the object is essentially different from the original. What makes this problem even worse it that everything about the object screams to programmers that it is just an extension of the original. If you have a different object, make a different object. Consider containing the original class instead of inheriting from it. It’s much clearer in the code when you explicitly refer to a method attached to a contained object rather than calling a virtual function.

What happens to code reuse? Yes, have some. I hate duplicating code; I’m a lazy typist and I’m very unlucky when it comes to cutting and pasting code. It also offends me.

Try to look at classes and their relationships like appliances and electrical cords. Always seek to minimize the length of the extension cords, minimize the appliances that plug into one another, and don’t make a nasty tangle that you have to figure out every time you want to turn something on. This metaphor is put into practice with a flat class hierarchy—one where you don’t have to open twelve source files to see all of the code for a particular class.

Use Interface Classes

Interface classes are those that contain nothing but pure virtual functions. They form the top level in any class hierarchy. Here’s an example:

class IAnimation
{
public:
   virtual void VAdvance(const int deltaMilliseconds) = 0;
   virtual bool const VAtEnd()const = 0;
   virtual int const VGetPosition()const = 0;
};
typedef std::list<IAnimation > AnimationList;

This sample interface class defines simple behavior common for a timed animation. We could add other methods such as one to tell how long the animation will run or whether the animation loops; that’s purely up to you. The point is that any system that contains a list of objects inheriting and implementing the’IAnimation interface can animate them with a few lines of code:

for(AnimationList::iterator itr = animList.begin();itr
                                != animList.end(); ++itr)
{
   (*itr).VAdvance(d elta );
}

Interface classes are a great way to enforce design standards. A programmer writing engine code can create systems that expect a certain interface. Any programmer creating objects that inherit from and implement the interface can be confidant that object will work with the engine code.

Consider Using Factories

Games tend to build screens and other complex objects constructing groups of objects, such as controls or sprites, and storing them in lists or other collections. A common way to do this is to have the constructor of one object, say a certain implementation of a screen class, “new up” all the sprites and controls. In many cases, many types of screens are used in a game, all having different objects inheriting from the same parents.

In the book, Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma et. al., one of the object creation patterns is called a factory. An abstract factory can define the interface for creating objects. Different implementations of the abstract factory carry out the concrete tasks of constructing objects with multiple parts. Think of it this way: a constructor creates a single object and a factory creates and assembles these objects into a working mechanism of some sort.

Imagine an abstract factory that builds screens. The fictional game engine in this example could define screens as components that have screen elements, a background, and a logic class that accepts control messages. Here’s an example:

class SaveGameScreenFactory : public IScreenFactory
{
public:
   SaveGameScreenFactory();

   virtual IScreenElements * const BuildScreenElements()const;
   virtual ScreenBackgroundSprite * const
           BuildScreenBackgroundSprite()const;
virtual IScreenLogic * const BuildScreenLogic()const;
};

The code that builds screens will call the methods of the IScreenFactory interface, each one returning the different objects that make the screen including screen elements such as buttons and sprites, a background, or the logic that runs the screen. As all interface classes tend to enforce design standards, factories tend to enforce orderly construction of complicated objects. Factories are great for screens, animations, AI, or any nontrivial game object.

What’s more, factories can help you construct these mechanisms at the right time. One of the neatest things about the factory design pattern is a delayed instantiation feature. You could create factory objects, push them into a queue, and delay calling the “BuildXYZ” methods until you were ready. In the screen example, you might not have enough memory to instantiate a screen object until the active one is destroyed. The factory object is tiny, perhaps a few tens of bytes, and can easily exist in memory until you are ready to fire it.

Use Streams to Initialize Objects

Any persistent object in your game should implement an overloaded constructor that takes a stream object as a parameter. If the game is loaded from a file, objects can use the stream as a source of parameters. Here’s an example to consider:

class AnimationPath
{
public:
   //...A better idea! Use a default constructor and an Init method ...
   AnimationPath();
   Initialize (InputStream & stream);
   Initialize (std::vector<AnimationPathPoint> const &srcPath);

   //Of course, lots more code follows.
};

This class has a default constructor, and two ways to initialize it. The first is through a classic parameter list, in this case a list of AnimationPathPoints. The second initializes the class through a stream object. This is cool because you can initialize objects from disk, a memory stream, or even the network. If you want to load game objects from disk, as you would in a saved game, this is exactly how you do it.

If you read the first edition of this book, perhaps you remember that this section suggested you use input streams in the constructor of an object, like this:

AnimationPath (InputStream &stream);

Boy that was a horrible idea, and I’m not too big to admit it either. The kind “corrections” posted on the web help me catch this one. The unkind ones I’ll happily forget! Here’s why It is a bad idea: a bad stream will cause your constructor to fail. You can never trust the content of a stream; it could be coming from a bad disk file, or even from hacked network packets. Ergo, construct objects with a default constructor you can rely on, and create initialization methods for streams.

Best Practice

Test your stream constructors by loading and saving your game automatically in the DEBUG build at regular intervals. It will have the added side effect of making sure programmers keep the load/save code pretty fast.

Smart Pointers and Naked Pointers

Clearly, all smart pointers wear clothing.

If you declare a pointer to another object, you’ve just used a naked pointer. Pointers are used to refer to another object, but they don’t convey enough information. Anything declared on the heap must be referenced by at least one other object or it can never be freed, causing a memory leak. It is common for an object on the heap to be referred multiple times by other objects in the code. A good example of this is a game object like a clock. A pointer to the clock will exist in the game object list, the physics system, the graphics system, and even the sound system.

If you use naked pointers you must remember which objects implicitly own other objects. An object that owns other objects controls their existence. Imagine a ship object that owns everything on the ship. When the ship sinks, everything else is destroyed along with it. If you use naked pointers to create these relationships you have to remember who owns who. This can be a confusing or even impossible task. You’ll find that using naked pointers will quickly paint you into a corner.

Smart pointers, on the other hand, hold extra information along with the address of the distant object. This information can count references, record permanent or temporary ownership, or perform other useful tasks. In a sense an object controlled by a smart pointer “knows” about every reference to itself. The horrible nest of naked pointers evaporates, leaving a simple and foolproof mechanism for handling your dynamic objects.

Reference Counting

Reference counting stores an integer value that counts how many other objects are currently referring to the object in question. Reference counting is a common mechanism in memory management. DirectX objects implement the COM based IUnknown interface, which uses reference counting Two methods that are central to this task are AddRef()and Release(). The following code shows how this works:

MySound *sound = new MySound;
sound->AddRef();               //reference count is now 1

After you construct a reference counted object, you call the AddRef() method to increase the integer reference counter by one. When the pointer variable goes out of scope, by normal scoping rules or by the destruction of the container class, you must call Release(). Release() will decrement the reference counter and destroy the object if the counter drops to zero. A shared object can have multiple references safely without fear of the object being destroyed, leaving bad pointers all over the place.

Best Practice

Good reference counting mechanisms automatically delete the object when the reference count becomes zero. If the API leaves the explicit destruction of the object to you, it’s easy to create memory leaks—all you have to do is forget to call Release(). You can also cause problems if you forget to call AddRef() when you create the object. It’s likely that the object will get destroyed unexpectedly, not having enough reference counts.

Anytime you assign a pointer variable to the address of the reference counted object you’ll do the same thing. This includes any calls inside a local loop:

for (int i=0; i<m_howMany; ++i)
{
   MySound *s = GoGrabASoundPointer(i);
   s->AddRef();

   DangerousFunction();

   if (s->IsPlaying())
   {
      DoSomethingElse();
   }

   s->Release();
}

This kind of code exists all over the place in every game I’ve ever worked on. The call to DangerousFunction() goes deep and performs some game logic that might attempt to destroy the instance of the MySound object. Don’t forget that in a release build that the deallocated memory retains the same values until it is reused. It’s quite possible that the loop will work just fine even though the MySound pointer is pointing to unallocated memory. What’s more likely to occur is a terrible corruption of memory.

Reference counting keeps the sound object around until Release() is called at the bottom of the loop. If there was only one reference to the sound before the loop started, the call to AddRef() will add one to the sound’s reference count, making two references. DangerousFunction() does something that destroys the sound, but through a call to Release(). As far as DangrousFunction() is concerned, the sound is gone forever. It still exists because one more reference to it, through MySound *s, kept the reference count from dropping to zero inside the loop. The final call to Release() causes the destruction of the sound.

Boost C++’s shared_ptr

If you think calling AddRef() and Release() all over the place might be a serious pain in the ass, you’re right. It’s really easy to forget an AddRef() or a Release() call, and your memory leak will be almost impossible to find. It turns out that there are plenty of C++ templates out there that implement reference counting in a way that handles the counter manipulation automatically. One of the best examples is the shared_ptr template class in the Boost C++ library, found at www.boost.org/.

Here’s an example on how to use this template:

#include <boost config.hpp>
#include <boost shared_ptr.hpp>

using boost::shared_ptr;


class IPrintable
{
public:
   virtual void VPrint()=0;
};

class CPrintable : public IPrintable
{
   char *m_Name;
public:
   CPrintable(char *name)  { m_Name = name;printf("create %s n",m_Name); }
   virtual ~CPrintable()   { printf("delete %s n",m_Name); }
   void VPrint()           { printf("print %s n",m_Name); }
};

shared_ptr<CPrintable> CreateAnObject(char *name)
{
   return shared_ptr<CPrintable>(new CPrintable(name));
}

void ProcessObject(shared_ptr<CPrintable> o)
{
   printf("(print from a function) ");
   o->VPrint();
}
void TestSharedPointers(void)
{
   shared_ptr<CPrintable> ptr1(new CPrintable("1"));  //create object 1
   shared_ptr>CPrintable> ptr2(new CPrintable("2"));  //create object 2

   ptr1 = ptr2;                                       //destroy object 1
   ptr2 = CreateAnObject("3")                         //used as a return value
   ProcessObject(ptr1);                               //call a function
   //BAD USEAGE EXAMPLES....
   //
   CPrintable o1("bad");
   //ptr1 =&o1;    //Syntax error!It's on the stack....
   //
   CPrintable *o2 = new CPrintable("bad2");
   //ptr1 = o2;    //Syntax error!Use the next line to do this...

   ptr1 = shared_ptr<CPrintable>(o2);

   //You can even use shared_ptr on ints!

   shared_ptr<int>a(new int);
   shared_ptr<int>b(new int);

   *a = 5;
   *b = 6;

   const int *q = a.get();    //use this for reading in multithreaded code
   //this is especially cool - you can also use it in lists.
   std::list< shared_ptr<int> > intList;
   std::list< shared_ptr<IPrintable> >printableList;
   for (int i=0; i<100; ++i)
   {
      intList.push_back(shared_ptr<int>(new int(rand())));
      printableList.push_back(shared_ptr<IPrintable>(new CPrintable("list")));
   }

   //No leaks!!!! Isn't that cool...
}

The template classes use overloaded assignment operators and copy operators to keep track of how many references point to the allocated data. As long as the shared_ptr object is in scope and you behave yourself by avoiding the bad usage cases you won’t leak memory and you won’t have to worry about objects getting destroyed while you are still referencing them from somewhere else.

This smart pointer even works in multithreaded environments as long as you follow a few rules. First, don’t write directly to the data. You can access the data through const operations such as the .get() method. As you can also see, the template works fine if it is inside an STL container such as std::list.

Best Practice

Don’t ignore multithreaded access to shared memory blocks. You might think that the chances of two threads accessing the shared data are exceedingly low, and convince yourself that you don’t need to go to the trouble of adding multithreaded protection. You’d be wrong, every time.

There are a couple of safety tips with smart pointers you should consider:

  • You can’t have two different objects manage each others’ smart pointers.
  • When you create a smart pointer, you have to make sure it is created straight from a raw pointer new operator.

I’ll show you examples of each of these abuses. If two objects have smart pointers to each other, neither one will ever be destroyed. It may take your brain a moment to get this since each one has a reference to the other:

class CJelly;
class CPeanutButter
{
public:
   shared_ptr<CJelly> m_pJelly;
   CPeanutButter(CJelly *pJelly) {m_pJelly.reset(pJelly);}
};

class CJelly
{
public:
   shared_ptr<CPeanutButter> m_pPeanutButter;
   CJelly();
};

CJelly::CJelly()
{
   m_pPeanutButter.reset(new CPeanutButter(this));
}

void PleaseLeakMyMemory()
{
   shared_ptr<CJelly> pJelly(new CJelly);
}

If you follow the code you’ll find that CJelly has two references, one from the free function and the other from the CPeanutButter. The CPeanutButter class only has one reference, but it can’t ever be decremented because the CJelly smart pointer will end up with a single reference count. Basically, because they point to each other, it’s almost like two stubborn gentlemen are saying, “No, sir, after you” and “Please, I insist” when trying to go through a single door. Because they point to each other, they will never be destroyed.

The solution to this is usually some kind of “owned” pointer or “weak referenced” pointer, where one object is deemed the de-factor owner, and therefore won’t use the multiply referenced shared_ptr mechanism.

The other gotcha is constructing two smart pointers to manage a single object:

int *z = new int;
shared_ptr<int> bad1(z);
shared_ptr<int> bad2(z);

Remember that smart pointers work with a reference count, and each of the smart pointer objects only has one reference. If either of them goes out of scope, the memory for the object will be deallocated, and the other smart pointer will point to garbage.

More to Come

This is part 1 of a 3 part series. The next installment will appear on April 4th.

About the Author

Mike McShaffry, a.k.a. “Mr. Mike,” started programming games as soon as he could tap a keyboard. He signed up at the University of Houston, where he graduated five and one-half years later. Then, he entered the boot camp of the computer game industry: Origin Systems. He worked for Warren Spector and Richard Garriott, a.k.a. “Lord British,” on many of their most popular games. In 1997, Mike formed his first company, Tornado Alley. He later took a steady gig at Glass Eye Entertainment, working for his friend Monty Kerr, where he produced Microsoft Casino.

About the Book

Game Coding Complete, Second Edition
By Mike McShaffry

Published: January 14, 2005, Paperback: 850 pages
Published by Paraglyph Press
ISBN: 1932111913
Retail price: $44.99

This material is from Chapter 3 of the book.

Paraglyph Press, copyright 2005, Game Coding Complete, 2nd Edition.
Reprinted with permission.

Get the Free Newsletter!
Subscribe to Developer Insider for top news, trends & analysis
This email address is invalid.
Get the Free Newsletter!
Subscribe to Developer Insider for top news, trends & analysis
This email address is invalid.

Latest Posts

Related Stories