|
| 1 | +# C++ API Guidelines |
| 2 | + |
| 3 | +**WIP** - *please feel free to improve* |
| 4 | + |
| 5 | +Intended for Firebase APIs, but also applicable to any C++ or Game APIs. |
| 6 | + |
| 7 | +# Code Style |
| 8 | + |
| 9 | +Please comply with the |
| 10 | +[Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html) |
| 11 | +as much as possible. Refresh your memory of this document before you start :) |
| 12 | + |
| 13 | +# C++ API Design |
| 14 | + |
| 15 | +### Don't force any particular usage pattern upon the client. |
| 16 | + |
| 17 | +C++ is a huge language, with a great variety of ways in which things can be |
| 18 | +done, compared to other languages. As a consequence, C++ projects can be very |
| 19 | +particular about what features of the language they use or don't use, how they |
| 20 | +represent their data, and structure their code. |
| 21 | + |
| 22 | +An API that forces the use of a feature or structure the client doesn't use |
| 23 | +will be very unwelcome. A good API uses only the simplest common denominator |
| 24 | +data types and features, and will be useable by all. This can generally be done |
| 25 | +with minimal impact on your API’s simplicity, or at least should form the |
| 26 | +baseline API. |
| 27 | + |
| 28 | +Examples of typical Do's and Don'ts: |
| 29 | + |
| 30 | +* Don't force the use of a particular data structure to supply or receive data. |
| 31 | + Typical examples: |
| 32 | + * `std::vector<T>`: If the client doesn't have the data already in a |
| 33 | + `std::vector` (or only wants to use part of a vector), they are forced |
| 34 | + to copy/allocate a new one, and C++ programmers don't like unnecessary |
| 35 | + copies/allocations. |
| 36 | + Instead, your primary interface should always take a |
| 37 | + `(const T *, size_t)` instead. You can still supply an optional helper |
| 38 | + method that takes a `std::vector<T> &` and then calls the former if |
| 39 | + you anticipate it to be called very frequently. |
| 40 | + * `std::string`: Unlike Java, these things aren't pooled, they're mutable |
| 41 | + and copied. A common mistake is to take a `const std::string &` argument, |
| 42 | + which forces all callers that supply a `const char *` to go thru a |
| 43 | + strlen+malloc+copy that is possibly of no use to the callee. Prefer to |
| 44 | + take a `const char *` instead for things that are names/identifiers, |
| 45 | + especially if they possibly are compile-time constant. If you're |
| 46 | + unsetting a string property, prefer to pass nullptr rather than an |
| 47 | + empty string. (There are |
| 48 | + [COW implementations](https://en.wikipedia.org/wiki/Copy-on-write), |
| 49 | + but you can't rely on that). |
| 50 | + * `std::map<K,V>`: This is a costly data structure involving many |
| 51 | + allocations. If all you wanted is for the caller to supply a list of |
| 52 | + key/value pairs, take a `const char **` (yes, 2 stars!). Or |
| 53 | + `const SimpleStruct *` instead, which allows the user to create this data |
| 54 | + statically. |
| 55 | +* Per-product configuration should be accomplished using an options struct |
| 56 | + passed to the library's `firebase::<library>::Initialize` function. Default |
| 57 | + options should be provided by the options struct's default constructor. The |
| 58 | + `Initialize` function should be overloaded with a version that does not take |
| 59 | + the options struct (which is how the Google style guide prefers that we pass |
| 60 | + default parameters). |
| 61 | + |
| 62 | + For example, |
| 63 | + |
| 64 | +```c++ |
| 65 | + struct LibraryOptions { |
| 66 | + LibraryOptions() : do_the_thing(false) {} |
| 67 | + |
| 68 | + bool do_the_thing; |
| 69 | + }; |
| 70 | + |
| 71 | + InitResult Initialize(const App& app) { |
| 72 | + return Initialize(app, LibraryOptions()); |
| 73 | + } |
| 74 | + InitResult Initialize(const App& app, const LibraryOptions& options); |
| 75 | +``` |
| 76 | +
|
| 77 | +* Don't make your client responsible for data you allocate or take ownership |
| 78 | + of client data. Typical C++ APIs are written such that both the client |
| 79 | + and the library own their own memory, and they take full responsibility for |
| 80 | + managing it, regardless of what the other does. Any data exchanged is |
| 81 | + typically done through weak references and copies. |
| 82 | + An exception may be a file loading API where buffers exchanged may be really |
| 83 | + big. If you are going to pass ownership, make this super obvious in all |
| 84 | + comments and documentation (C++ programmers typically won't expect it), |
| 85 | + and document which function should be used to free the data (free, delete, |
| 86 | + or a custom one). |
| 87 | + Alternatively, a simple way to pass ownership of a large new buffer to the |
| 88 | + client is to ask the client to supply a std::string *, which you then |
| 89 | + resize(), and write directly into its owned memory. This somewhat violates |
| 90 | + the rule about use of std::string above, though. |
| 91 | +
|
| 92 | +* Don't use exceptions. This one is worth mentioning separately. Though |
| 93 | + exceptions are great in theory, in C++ hardly any libraries use them, and |
| 94 | + many code-bases disallow them entirely. They also require the use of RTTI |
| 95 | + which some environments turn off. Oh, yes, also don't use RTTI. |
| 96 | +
|
| 97 | +* Go easy on templates when possible. Yes, they make your code more general, |
| 98 | + but they also pull a lot of implementation detail into your API, lengthen |
| 99 | + compile times and bloat binaries. In C++ they are glorified macros, so they |
| 100 | + result in hard to understand errors, and can make correct use of your API |
| 101 | + harder to understand. |
| 102 | +
|
| 103 | +* Utilize C++11 features where appropriate. This project has adopted C++11, |
| 104 | + and features such as `std::unique_ptr`, `std::shared_ptr`, `std::make_unique`, |
| 105 | + and `std::move` are encouraged to improve code safety and readability. |
| 106 | + However, avoid features from C++14 or newer standards. |
| 107 | +
|
| 108 | +* Go easy on objectifying everything, and prefer value types. In languages |
| 109 | + like Java it is common to give each "concept" your API deals with its own |
| 110 | + class, such that methods on it have a nice home. In C++ this isn't always |
| 111 | + desirable, because objects need to be managed, stored and allocated, and you |
| 112 | + run into ownership/lifetime questions mentioned above. Instead: |
| 113 | +
|
| 114 | + * For simple data, prefer their management to happen in the parent class |
| 115 | + that owns them. Actions on them are methods in the parent. If at all |
| 116 | + possible, prefer not to refer to them by pointer/reference (which |
| 117 | + creates ownership and lifetime issues) but by index/id, or string |
| 118 | + if not performance sensitive (for example, when referring to file |
| 119 | + resources, since the cost of loading a file dwarfs the cost of a string |
| 120 | + lookup). |
| 121 | +
|
| 122 | + * If you must create objects, and objects are not heavyweight (only scalars |
| 123 | + and non-owned pointers), make use of these objects by value (return by |
| 124 | + value, receive by const reference). This makes ownership and lifetime |
| 125 | + management trivial and efficient. |
| 126 | +
|
| 127 | +* If at all possible, don't depend on external libraries. C++ compilation, |
| 128 | + linking, dependency management, testing (especially cross platform) are |
| 129 | + generally way harder than any other language. Every dependency is a potential |
| 130 | + source of build complexity, conflicts, efficiency issues, and in general more |
| 131 | + dependencies means less adoption. |
| 132 | +
|
| 133 | + * Don't pull in large libraries (e.g. BOOST) just for your convenience, |
| 134 | + especially if their use is exposed in headers. |
| 135 | +
|
| 136 | + * Only use external libraries that have hard to replicate essential |
| 137 | + functionality (e.g. compression, serialization, image loading, networking |
| 138 | + etc.). Make sure to only access them in implementation files. |
| 139 | +
|
| 140 | + * If possible, make a dependency optional, e.g. if what your API does |
| 141 | + benefits from compression, make the client responsible for doing so, |
| 142 | + or add an interface for it. Add sample glue code or an optional API for |
| 143 | + working with the external library that is by default off in the build |
| 144 | + files, and can be switched on if desired. |
| 145 | +
|
| 146 | +* Take cross-platform-ness seriously: design the API to work on ALL platforms |
| 147 | + even if you don't intend to supply implementations for all. Hide platform |
| 148 | + issues in the implementation. Don't ever include platform specific headers in |
| 149 | + your own headers. Have graceful fallback for platforms you don't support, such |
| 150 | + that some level of building / testing can happen anywhere. |
| 151 | +
|
| 152 | +* If your API is meant to be VERY widespread in use, VERY general, and very |
| 153 | + simple (e.g. a compression API), consider making at least the API (if not all |
| 154 | + of it) in C, as you'll reach an even wider audience. C has a more consistent |
| 155 | + ABI and is easier to access from a variety of systems / languages. This is |
| 156 | + especially useful if the library implementation is intended to be provided in |
| 157 | + binary. |
| 158 | +
|
| 159 | +* Be careful not to to use idioms from other languages that may be foreign to |
| 160 | + C++. |
| 161 | +
|
| 162 | + * An example of this is a "Builder" API (common in Java). Prefer to use |
| 163 | + regular constructors, with additional set_ methods for less common |
| 164 | + parameters if the number of them gets overwhelming. |
| 165 | +
|
| 166 | +* Do not expose your own UI to the user as part of an API. Give the developer |
| 167 | + the data to work with, and let them handle displaying it to the user in the |
| 168 | + way they see fit. |
| 169 | +
|
| 170 | + * Rare exceptions can be made to this rule on a case-by-case basis. For |
| 171 | + example, authentication libraries may need to display a sign-in UI for the |
| 172 | + user to enter their credentials. Your API may work with data owned by |
| 173 | + Google or by the user (e.g. the user's contacts) that we don't want to |
| 174 | + expose to the app; in those cases, it is appropriate to expose a UI (but |
| 175 | + to limit the scope of the UI to the minimum necessary). |
| 176 | +
|
| 177 | + * In these types of exceptional cases, the UI should be in an isolated |
| 178 | + component, separate from the rest of the API. We do allow UIs to be |
| 179 | + exposed to the user UI-specific libraries, e.g. FirebaseUI, which should |
| 180 | + be open-source so developers can apply any customizations they need. |
| 181 | +
|
| 182 | +# Game API Design |
| 183 | +
|
| 184 | +### Performance matters |
| 185 | +
|
| 186 | +Most of this is already encoded in C++ API design above, but it bears repeating: |
| 187 | +C++ game programmers can be more fanatic about performance than you expect. |
| 188 | +
|
| 189 | +It is easy to add a layer of usability on top of fast code, it is very hard to |
| 190 | +impossible to "add performance" to an API that has performance issues baked into |
| 191 | +its design. |
| 192 | +
|
| 193 | +### Don't rely on state persisting for longer than one frame. |
| 194 | +
|
| 195 | +Games have an interesting program structure very unlike apps or web pages: they |
| 196 | +do all processing (and rendering) of almost all functionality of the game within |
| 197 | +a *frame* (usually 1/60th of a second), and then start anew for the next frame. |
| 198 | +
|
| 199 | +It is common for all or part of the state of a game to be wiped out from one |
| 200 | +frame to the next (e.g when going into the menu, loading a new level, starting a |
| 201 | +cut-scene..). |
| 202 | +
|
| 203 | +The consequence of this is that the state kept between frames is the only record |
| 204 | +of what is currently going on, and that managing this state is a source of |
| 205 | +complexity, especially when part of it is reflected in external code: |
| 206 | +
|
| 207 | +* Prefer API design that is stateless, or if it is stateful, is so only within a |
| 208 | + frame (i.e. between the start of the frame and the start of the next one). |
| 209 | + This really simplifies the client's use of your API: they can't forget to |
| 210 | + "reset" your API's state whenever they change state themselves. |
| 211 | +
|
| 212 | +* Prefer not to use cross-frame callbacks at all (non-escaping callbacks are |
| 213 | + fine). Callbacks can be problematic in other contexts, but they're even more |
| 214 | + problematic in games. Since they will execute at a future time, there's no |
| 215 | + guarantee that the state that was there when the callback started will still |
| 216 | + be there. There's no easy way to robustly "clear" pending callbacks that don't |
| 217 | + make sense anymore when state changes. Instead, make your API based on |
| 218 | + *polling*. |
| 219 | + Yes, everyone learned in school that polling is bad because it uses CPU, but |
| 220 | + that's what games are based on anyway: they check a LOT of things every frame |
| 221 | + (and only at 60hz, which is very friendly compared to busy-wait polling). If |
| 222 | + your API can be in various states of a state machine (e.g. a networking based |
| 223 | + API), make sure the client can poll the state you're in. This can then easily |
| 224 | + be translated to user feedback. |
| 225 | + If you have to use asynchronous callbacks, see the section on async operations |
| 226 | + below. |
| 227 | +
|
| 228 | +* Be robust to the client needing to change state. If work done in your API |
| 229 | + involves multiple steps, and the client never gets to the end of those steps |
| 230 | + before starting a new sequence, don't be "stuck", but deal with this |
| 231 | + gracefully. If the game's state got reset, it will have no record of what it |
| 232 | + was doing before. Try to not make the client responsible for knowing what it |
| 233 | + was doing. |
| 234 | +
|
| 235 | +* Interaction with threading: |
| 236 | +
|
| 237 | + * If you are going to use threading at all, make sure the use of that is |
| 238 | + internal to the library, and any issues of thread-safety don't leak into |
| 239 | + the client. Allow what appears to be synchronous access to a possibly |
| 240 | + asynchronous implementation. If the asynchronous nature will be |
| 241 | + externally visible, see the section on async operations below. |
| 242 | +
|
| 243 | + * Games are typically hard to thread (since it’s hard to combine with its |
| 244 | + per-frame nature), so the client typically should have full control over |
| 245 | + it: it is often better to make a fully synchronous single-threaded library |
| 246 | + and leave threading it to the client. Do not try to make your API itself |
| 247 | + thread-safe, as your API is unlikely the threading boundary (if your |
| 248 | + client is threaded, use of your library is typically isolated to one |
| 249 | + thread, and they do not want to pay for synchronization cost they don't |
| 250 | + use). |
| 251 | +
|
| 252 | + * When you do spin up threads to reduce a workload, it is often a good idea |
| 253 | + to do that once per frame, as avoid the above mentioned state based |
| 254 | + problems, and while starting threads isn't cheap, you may find it not a |
| 255 | + problem to do 60x per second. Alternatively you can pool them, and make |
| 256 | + sure you have an explicit way to wait for their idleness at the end of a |
| 257 | + frame. |
| 258 | +
|
| 259 | +* Games typically use their own memory allocator (for efficiency, but also to be |
| 260 | + able to control and budget usage on memory constrained systems). For this |
| 261 | + reason, most game APIs tend to provide allocation hooks that will be used for |
| 262 | + all internal allocation. This is even more important if you wish to be able to |
| 263 | + transfer ownership of memory. |
| 264 | +
|
| 265 | +* Generally prefer solutions that are low on total memory usage. Games are |
| 266 | + always constrained on memory, and having your game be killed by the OS because |
| 267 | + the library you use has decided it is efficient to cache everything is |
| 268 | + problematic. |
| 269 | +
|
| 270 | + * Prefer to recompute values when possible. |
| 271 | +
|
| 272 | + * When you do cache, give the client control over total memory used for |
| 273 | + this purpose. |
| 274 | +
|
| 275 | + * Your memory usage should be predictable and ideally have no peaks. |
| 276 | +
|
| 277 | +# Async Operations |
| 278 | +
|
| 279 | +### Application Initiated Async Operations |
| 280 | +
|
| 281 | +* Use the Future / State Pattern. |
| 282 | +* Add a `*Result()` method for each async operation method to allow the caller |
| 283 | + to poll and not save state. |
| 284 | +
|
| 285 | +e.g. |
| 286 | +
|
| 287 | +```c++ |
| 288 | + // Start async operation. |
| 289 | + Future<SignInWithCrendentialResult> SignInWithCredential(...); |
| 290 | + // Get the result of the pending / last async operation for the method. |
| 291 | + Future<SignInWithCrendentialResult> GetSignInWithCredentialResult(); |
| 292 | +
|
| 293 | + Usage examples: |
| 294 | + // call and register callback |
| 295 | + auto& result = SignInWithCredential(); |
| 296 | + result.set_callback([](result) { if (result == kComplete) { do_something_neat(); wake_up(); } }); |
| 297 | + // wait |
| 298 | +
|
| 299 | + // call and poll #1 (saving result) |
| 300 | + auto& result = SignInWithCredential(); |
| 301 | + while (result.value() != kComplete) { |
| 302 | + // wait |
| 303 | + } |
| 304 | +
|
| 305 | + // call and poll #2 (result stored in API) |
| 306 | + SignInWithCredential(); |
| 307 | + while (GetSignInWithCredentialResult().value() != kComplete) { |
| 308 | + } |
| 309 | +``` |
| 310 | + |
| 311 | +### API Initiated Async Event Handling |
| 312 | + |
| 313 | +* Follow the |
| 314 | + [listener / observer pattern](https://en.wikipedia.org/wiki/Observer_pattern) |
| 315 | + for API initiated (i.e where the caller doesn't initiate the event) |
| 316 | + async events. |
| 317 | +* Provide a queued interface to allow users to poll for events. |
| 318 | + |
| 319 | +e.g. |
| 320 | + |
| 321 | +```c++ |
| 322 | + class GcmListener { |
| 323 | + public: |
| 324 | + virtual void OnDeletedMessage() {} |
| 325 | + virtual void OnMessageReceived(const MessageReceivedData* data) {} |
| 326 | + }; |
| 327 | + |
| 328 | + class GcmListenerQueue : private GcmListener { |
| 329 | + public: |
| 330 | + enum EventType { |
| 331 | + kEventTypeMessageDeleted, |
| 332 | + kEventTypeMessageReceived, |
| 333 | + }; |
| 334 | + |
| 335 | + struct Event { |
| 336 | + EventType type; |
| 337 | + MessageReceivedData data; // Set when type == kEventTypeMessageReceived |
| 338 | + }; |
| 339 | + |
| 340 | + // Returns true when an event is retrieved from the queue. |
| 341 | + bool PollEvent(Event *event); |
| 342 | + }; |
| 343 | + |
| 344 | + // Wait for callbacks |
| 345 | + class MyListener : public GcmListener { |
| 346 | + public: |
| 347 | + virtual void OnDeletedMessage() { /* do stuff */ } |
| 348 | + virtual void OnMessageReceived() { /* display message */ } |
| 349 | + }; |
| 350 | + MyListener listener; |
| 351 | + gcm::Initialize(app, &listener); |
| 352 | + |
| 353 | + // Poll |
| 354 | + GcmListenerQueue queued_listener; |
| 355 | + gcm::Initialize(app, &queued_listener); |
| 356 | + GcmListenerQueue::Event event; |
| 357 | + while (queued_listener(&event)) { |
| 358 | + switch (event.type) { |
| 359 | + case kEventTypeMessageDeleted: |
| 360 | + // do stuff |
| 361 | + break; |
| 362 | + case kEventTypeMessageReceived: |
| 363 | + // display event.data |
| 364 | + break; |
| 365 | + } |
| 366 | + } |
| 367 | +``` |
| 368 | +
|
| 369 | +# Data Types |
| 370 | +
|
| 371 | +* Time: [Milliseconds since the epoch](https://en.wikipedia.org/wiki/Unix_time) |
| 372 | +
|
| 373 | +# More on C++ and games |
| 374 | +
|
| 375 | +* Performance Oriented C++ for Game Developers (very incomplete). |
0 commit comments