Categories
Software Development Web Development

Why I Will Always Choose Firefox

Unlike most people who may be reading this, when I install a browser, it’s always Firefox. Being a software developer, and working around other tech-literate people, this simple fact seems to draw a fair bit of confusion, as everybody else seems to think of Google Chrome as their go-to option.

I’ve had a great many friends over the years try to convince me to switch to Chrome, usually, I presume, as an instinctive aversion to difference, and once because they wanted to prank me by installing the NCage chrome extension. On that last occasion, I installed it just to see the prank, and then promptly uninstalled it again.

In the past, my reasoning for doing so wasn’t so strong. When I first installed Firefox, it was when I was perhaps in junior high school, and at the time I wasn’t very tech literate. I had basically every browser installed on my old Windows XP system, and any given week I might favor a different one. At one point, I even recall being a staunch Safari for Windows supporter. Perhaps that’s slightly less of a sin than my occasional use of Internet Explorer, though.

It was a few years later, when I was in high school, that I finally made my final decision. I wanted to only use Firefox… because of the cute fox mascot (I am aware that a fire fox is a red panda, but their mascot isn’t, so your point is invalid). No joke, that was the decision that led to where I am today.

Things have changed an awful lot since I made up my mind about my choice of browser, but I have used every major browser there is, and I am still up to date on the technological details and features of most of them. To this day, I feel that I still made the correct choice, albeit for a silly reason at the time.

In the past, I felt happy to be different in my choice, because why not? Everybody else could use whatever they wanted, and so could I. The nature of the internet wasn’t at stake. But now, that has changed. With Microsoft choosing to switch edge to be based off of Chromium, this now means that there are only two remaining players in the browser engine arena: Google’s Chromium, and Mozilla’s Gecko.

Competition Makes Better Browsers

One of the benefits to what may seem like duplicated work is a parallel to one of the main benefits of capitalism in general: competition creates faster innovation. Because every browser team wanted theirs to be the best, they had no choice but to try to innovate faster, create richer features, and do whatever the people wanted as fast as possible, lest they fall out of favor and be replaced by their competitor. No doubt this is, and was, a stressful thing, but that stress undoubtedly yields a superior product from everyone in the game.

If Gecko were to fall, there would be only one browser engine left: Chromium. Do you think they’d need to innovate quickly? Absolutely not. They’d be in no danger of being replaced. Innovation would surely still continue, but not at the rate it has in the past.

Competition Enforces Standards

The web is entirely based on standardization right now. There are several large committees representing hundreds of companies and organizations that govern the standards of the web that we use today. These standards are designed to be written guidelines with very specific rules, which ensure that multiple different implementations produce the same result when given the same web content.

In the past, this has worked very well. Despite the occasional difficulty, browser engines were able to keep up with these standards, and that’s why somebody could create one version of their website for Chrome, Firefox, iOS, Opera, Edge, and so on, and it would work the same everywhere (except Internet Explorer, they could never truly get it right

The question it seems a lot of people might struggle to answer is why every browser cared so much about following standards. Why couldn’t chrome create its own method of drawing on a canvas, or aligning content, for example? Surely doing so would give them the ability to claim that they had a special, extra feature that nobody else did. In a way, it’d be much like Microsoft holding exclusivity on an API like DirectX. Anybody wanting to use that API needed Windows (until Wine happened, of course). The problem, which is on a smaller scale an issue faced by DirectX, is that developers will begin to prefer alternative APIs that support more browsers with the same code, and users of other browsers will begin to see that browser as something of a non-team-player. Every time they go to a website that doesn’t work correctly because of the lack of standardization, they’ll blame both the developer of the site, and the developer of the non-standard browser. Further, if the other browsers are all standardizing, and one isn’t, then they all become the browsers that “Just Work” and the odd one out is… well… the odd one out.

I do believe that the initial adoption of web standards was because browser developers are web developers too, and they likely did it out of a desire to make the web a better place, but years later, standards are no longer optional, and competition is why.

If Gecko dropped out of the competition and left Chromium all alone, would the Chromium team have to follow standards? Absolutely not. I believe they would try, but they’d have all the leverage. If they wrote an API, it’d become a de facto standard, because everybody has it. If they wrote an implementation wrong, it wouldn’t matter, because theirs would be the vast majority of the market share, so the actual standard implementation would actually become “wrong.” This might sound good to some out there, because it would surely streamline the process of the standards, but it puts all of the power in the hands of one project, one team, one company. It’d essentially be giving Google a monopoly on the internet. They wouldn’t necessarily use that for evil, the problem is that they easily could.

The Fight

Maybe I’m too idealistic, but now that Gecko is the last competitor to Chromium, this has become a real fight to me. I won’t switch to Chrome because I like Firefox more, but also because I believe in the open, competitive web, and that can only change over my cold, dead Gecko-based browser. And I hope some of you will join me in that fight.

Categories
Game Development Software Architecture Software Development

The Scourge of Global Mutable State

I’m not a perfect programmer, but I like to think that I’m getting better every day. I’ll admit that one of the things that I’ve had the most difficult time adapting to was what I perceived as other programmers being somewhat pedantic about avoiding what they called “global state” or sometimes more specifically “global mutable state.”

In the past, I held onto some conviction that while this is something to be avoided where possible, there are actually cases where it is appropriate. Recently I’ve decided to not only better conform to this well-known best-practice, but also to understand why it is that this is an issue, and what can be done to achieve what I, in the past, mistakenly perceived as the reason that global mutable state made sense, without violating this fundamental rule.

To learn something, you must learn the rules, but to become an expert, you must learn when to break these same rules. Programming is no different. However, over several years of programming I’ve come to find that the “when” of “when to break the rules” is an exception, not the rule. If you’re breaking the rule more often than not, then you’re probably misunderstanding something or being lazy. It pays to heed the words of the experts superior to you in experience even when you don’t fully understand why, because eventually, with more experience, you will understand why, and if you failed to heed their advice, you’ll find yourself chastising your past self for it.

Let me start by explaining why it is that I’ve so often resisted this best practice and gone ahead with global mutable state anyway. When writing larger code bases, which most projects inevitably aspire to be, you will find that some of your code will be, as a matter of necessity, used by a large proportion of the code base. Using dependency injection as your only distribution technique, as an example, might drastically increase the verbosity of your code. Who wants to pass an instance a “Log” class to every single instance of a “GameState” object? I’ll admit that it’s not really THAT much extra work, but it has a distinct feeling of not being “DRY,” since I’m essentially repeating myself on every object instantiation. There are some subsystems which, by nature, the entire system will need to access, and would be better off being organized.

A pet peeve not only of myself, but of many people, is that some people seem to think that using the singleton pattern is a solution to this problem. Singletons are global state, and moreover, it appears to me that by now, the singleton is obsolete. Any problem solvable with a singleton can be solved with a static class with mutable private fields, and frankly these static classes don’t lie about their true nature. But both are global state, and both ought to be avoided anyway. But if you’re going to ignore the advice anyway, you might as well just use the static class instead, so users can recognize the warning signs of nondeterministic behavior before it’s too late.

I’ve learned, however, that all of my arguments came from a lack of understanding of the actual problem, and the actual solutions.

  • Global mutable state generally excludes communication channels that allow decoupled non-global objects to communicate (such as Observers or Service Locators). This is a significant solution to this problem.
  • Global mutable state implies that the global items in question contain actual values that change between subsequent calls. Global classes are still perfectly valid for resolving the problem of subsystems that need to be accessed from across the system as whole, so long as they incorporate the solutions from the previous bullet.

I’ll document one solution that I solved with my TimberWolf game engine.

In the past, I’ve had a static class named Log, which was declared as follows:

#ifndef H_CLASS_LOG
#define H_CLASS_LOG

#include <iostream>
#include <string>
#include <iomanip>
#include <sstream>
#include <fstream>
#include <ctime>
#include <mutex>
#include <exception>
#include <cstdlib>
#include <atomic>
#include "../../enum/LogLevel/LogLevel.hpp"

class Log {

public:

    Log () = delete; // static only
    ~Log () = delete;

    Log (Log&&) = delete;
    Log& operator = (Log&&) = delete;

    Log (const Log&) = delete;
    Log& operator = (const Log&) = delete;

    static bool cliOutputEnabled ();
    static void enableCliOutput ();
    static void disableCliOutput ();

    static LogLevel getCliFilterLevel ();
    static void setCliFilterLevel (LogLevel);

    static bool fileOpen ();
    static bool openFile (const std::string&);
    static void closeFile ();

    static bool fileOutputEnabled ();
    static void enableFileOutput ();
    static void disableFileOutput ();

    static LogLevel getFileFilterLevel ();
    static void setFileFilterLevel (LogLevel);

    template<typename ...T>
    static void log (LogLevel messageType, T&&... message) {

        std::string out;

        if (
            (m_cliOutputEnabled || m_fileOutputEnabled) &&
            (
                messageType == LogLevel::UNDEFINED ||
                messageType >= m_cliFilterLevel ||
                messageType >= m_fileFilterLevel
            )
        ) {
            out = Log::formatMessage(messageType, Log::concatMessage(std::forward<T>(message)...));
        } else {
            return;
        }

        if (m_cliOutputEnabled) {
            if (messageType == LogLevel::UNDEFINED || messageType >= m_cliFilterLevel) {

                if (messageType == LogLevel::ERROR) {
                    std::unique_lock<std::mutex> lock_stderr(Log::mutex_stderr);
                    std::cerr << out << std::endl;
                } else {
                    std::unique_lock<std::mutex> lock_stdout(Log::mutex_stdout);
                    std::cout << out << std::endl;
                }

            }
        }

        if (m_fileOutputEnabled && fileOpen()) {
            if (messageType == LogLevel::UNDEFINED || messageType >= m_fileFilterLevel) {

                std::unique_lock<std::mutex> lock_file(Log::mutex_file);
                m_file << out << std::endl;

            }
        }

    }

    template<typename ...T>
    static void verbose (T&&... message) {
        Log::log(LogLevel::VERBOSE, std::forward<T>(message)...);
    }

    template<typename ...T>
    static void notice (T&&... message) {
        Log::log(LogLevel::NOTICE, std::forward<T>(message)...);
    }

    template<typename ...T>
    static void warning (T&&... message) {
        Log::log(LogLevel::WARNING, std::forward<T>(message)...);
    }

    template<typename ...T>
    static void error (T&&... message) {
        Log::log(LogLevel::ERROR, std::forward<T>(message)...);
    }

    static void bindUnhandledException ();
    static void unhandledException ();

private:

    template<typename ...T>
    static std::string concatMessage (T&&... message) {

        std::ostringstream oss;
        (oss << ... << std::forward<T>(message));
        return oss.str();

    }

    static std::string formatMessage (LogLevel, const std::string&);

    static std::atomic<bool> m_cliOutputEnabled;
    static std::atomic<bool> m_fileOutputEnabled;

    static std::atomic<LogLevel> m_cliFilterLevel;
    static std::atomic<LogLevel> m_fileFilterLevel;

    static std::ofstream m_file;

    static std::mutex mutex_stdout;
    static std::mutex mutex_stderr;
    static std::mutex mutex_file;

};

#endif

As you can see, everything is static, and the class cannot be instantiated. This makes sense, because the Log class is something that needs to be accessed from all over the code base, and we generally want all log messages to funnel through a single location so none of them accidentally go astray before they are sent or stored. This was all fine and dandy when it was originally written, because it only had one purpose then, and that was to write error messages to std::cout or std::cerr. It interacted with a global I/O, but it didn’t store any kind of state.

After a while, though, I decided that a few features would be nice to have in the logging subsystem. First of all, I wanted to be able to filter log messages by their LogLevel enum. This was simple enough. Add a static field to store the minimum log level, and then compare the log level of each message before writing it to I/O. But now the log class contains the global state for the current log level. Anybody could change that, and it might make it hard to debug stuff because its behavior becomes nondeterministic. The behavior of the Log class depends on the order of modifications. If some code somewhere deep in the code base calls Log::setFilterLevel(LogLevel::ERROR) because the developer was getting inundated with verbose calls and wanted to temporarily silence them, and then forgot to remove it, all LogLevel::WARNING messages sent after that piece of code gets invoked will fail to display. The developer might mistakenly think that no errors are occurring. And there’s no way to receive those messages elsewhere unless the filter level is dropped again.

This hasn’t actually been a problem for me yet, but I was slightly aware of the possibility, but I hadn’t yet done anything to resolve it. I added additional features, such as allowing Log to write to a log file, and allowing a separate filter level for the log file. I noticed some additional potential problems after adding these features, however. First of all, I can only write to a single log file. What if I decide later on that I want to write only warnings and errors to one file, and all messages to another file. Or what if I want a console window in my game somewhere to display the error messages, without requiring the end-user to look at their terminal or open up a log file?

The Log subsystem is a problem that can be solved by the observer pattern. I can leave the API for sending log messages alone, because it implements everything the observables need (but I am going to add a context string that can be used for additional filtering, in case we want the log messages pertaining only to the sound system to go somewhere, for example). There will need to be a separate API for RECEIVING the log messages, however. One of these will be used to receive messages and route them to the command line. Another one will be used to put messages into a file. But more can be created and removed as needed.

If those changes are implemented, The Log class, which is the global portion of the subsystem, will no longer contain any global state at all (the vectors of observers are not technically “state” in this context, as they are not going to have any effect on one another, the log subsystem, or the game as a whole). Any code in the code base can send messages to it, but the output, which is the real tangible effect of the log subsystem, will only be controlled by non-global objects. Code far across the code base will not be able to create nondeterministic behavior in the logs of an unrelated system.

This is, for all intents and purposes, the best of both worlds. I can expose a global API without needing to define behavior globally. I can create a log subsystem that doesn’t require dependency injection, but also doesn’t expose any state to the global scope, while still supporting stateful behavior at one end of the pipeline.

With that explanation out of the way now, allow me to show you the finished solution. Note that between the old version and this current one, the namespace “tw” was added to everything.

Here’s the new Log class declaration:

#ifndef H_CLASS_LOG
#define H_CLASS_LOG

#include <iostream>
#include <string>
#include <vector>
#include <memory>
#include <sstream>
#include <mutex>
#include <functional>
#include "../LogObserver/LogObserver.hpp"
#include "../../enum/LogLevel/LogLevel.hpp"

namespace tw {
class Log {

public:

    Log () = delete; // static only
    ~Log () = delete;

    Log (Log&&) = delete;
    Log& operator = (Log&&) = delete;

    Log (const Log&) = delete;
    Log& operator = (const Log&) = delete;

    template <typename ...T>
    static void log (LogLevel messageType, const std::string& context, T&&... message) {

        std::unique_lock<std::mutex> lock(m_mutex);

        for (unsigned int i = 0; i < m_observers.size(); ++i) {

            m_observers[i]->notify(messageType, context, concatMessage(std::forward<T>(message)...));

        }

    }

    template <typename ...T>
    static void verbose (const std::string& context, T&&... message) {
        Log::log(LogLevel::VERBOSE, context, std::forward<T>(message)...);
    }

    template <typename ...T>
    static void notice (const std::string& context, T&&... message) {
        Log::log(LogLevel::NOTICE, context, std::forward<T>(message)...);
    }

    template <typename ...T>
    static void warning (const std::string& context, T&&... message) {
        Log::log(LogLevel::WARNING, context, std::forward<T>(message)...);
    }

    template <typename ...T>
    static void error (const std::string& context, T&&... message) {
        Log::log(LogLevel::ERROR, context, std::forward<T>(message)...);
    }

    static void bindUnhandledException ();

    template <typename T, typename ...Targ>
    static LogObserver* makeObserver (Targ&&... args) {

        auto observer = std::make_unique<T>(std::forward<Targ>(args)...);
        auto observerPtr = observer.get();

        m_observers.push_back(std::move(observer));

        return observerPtr;

    }
    static void registerObserver (std::unique_ptr<LogObserver>&&);
    static void registerObserver (LogObserver*);

private:

    template <typename ...T>
    static std::string concatMessage (T&&... message) {

        std::ostringstream oss;
        (oss << ... << std::forward<T>(message));
        return oss.str();

    }

    static std::mutex m_mutex;
    static std::vector<std::unique_ptr<LogObserver>> m_observers;

};
}

#endif

As you can see, the Log class is now nothing more than a shell, a notification system for a collection of observers (and also a system for creating and managing said observers). It presents a new class known as LogObserver, which is a virtual class.

#ifndef H_CLASS_LOGOBSERVER
#define H_CLASS_LOGOBSERVER

#include <string>
#include <set>
#include "../../enum/LogLevel/LogLevel.hpp"
namespace tw{ class Log; }

namespace tw {
class LogObserver {

// WARNING: Never call Log::* functions from inside subclasses of this.
// It will cause infinite recursion and you will be sad.

public:

    static inline constexpr unsigned int ALLOW_UNDEFINED = 0b00001;
    static inline constexpr unsigned int ALLOW_VERBOSE =   0b00010;
    static inline constexpr unsigned int ALLOW_NOTICE =    0b00100;
    static inline constexpr unsigned int ALLOW_WARNING =   0b01000;
    static inline constexpr unsigned int ALLOW_ERROR =     0b10000;

    LogObserver () = default;
    explicit LogObserver (unsigned int);
    explicit LogObserver (const std::set<std::string>&);
    explicit LogObserver (const std::string&...);
    LogObserver (unsigned int, const std::set<std::string>&);
    LogObserver (unsigned int, const std::string&...);
    virtual ~LogObserver () = default;

    LogObserver (LogObserver&&) = default;
    LogObserver& operator = (LogObserver&&) = default;

    LogObserver (const LogObserver&) = default;
    LogObserver& operator = (const LogObserver&) = default;

    void notify (LogLevel, const std::string&, const std::string&);

    bool undefinedAllowed () const;
    void allowUndefined ();
    void blockUndefined ();
    bool verboseAllowed () const;
    void allowVerbose ();
    void blockVerbose ();
    bool noticeAllowed () const;
    void allowNotice ();
    void blockNotice ();
    bool warningAllowed () const;
    void allowWarning ();
    void blockWarning ();
    bool errorAllowed () const;
    void allowError ();
    void blockError ();

    bool contextAllowed (const std::string&...) const;
    void allowContext (const std::string&...);
    void blockContext (const std::string&...);
    bool allContextAllowed () const;
    void allowAllContext ();
    void restrictContext ();

protected:

    virtual void notifyCallback (LogLevel, const std::string&, const std::string&) = 0;

    unsigned int m_allowedLevelFlags {
        ALLOW_UNDEFINED |
        ALLOW_VERBOSE |
        ALLOW_NOTICE |
        ALLOW_WARNING |
        ALLOW_ERROR
    };

    std::set<std::string> m_allowedContexts;
    bool m_allContextsAllowed {true};

};
}

#endif

LogObserver classes contain non-overrideable fields and methods for storing and manipulating data about the log levels and contexts that it’s interested in. The notify method is markedly non-virtual because it implements the code for determining if the LogObserver even cares about the message that’s being sent to notify. As such, notify is the method that is initially invoked whenever a log message is being processed. If all of the checks pass, then it is finally forwarded on to the protected virtual method notifyCallback, which is to be overridden to the tastes and needs of the implementor.

Let’s look at some examples of implementations, the few that I’ve already made, perhaps all that I’ll ever need:

#ifndef H_CLASS_LOGOBSERVER
#define H_CLASS_LOGOBSERVER

#include <string>
#include <set>
#include "../../enum/LogLevel/LogLevel.hpp"
namespace tw{ class Log; }

namespace tw {
class LogObserver {

// WARNING: Never call Log::* functions from inside subclasses of this.
// It will cause infinite recursion and you will be sad.

public:

    static inline constexpr unsigned int ALLOW_UNDEFINED = 0b00001;
    static inline constexpr unsigned int ALLOW_VERBOSE =   0b00010;
    static inline constexpr unsigned int ALLOW_NOTICE =    0b00100;
    static inline constexpr unsigned int ALLOW_WARNING =   0b01000;
    static inline constexpr unsigned int ALLOW_ERROR =     0b10000;

    LogObserver () = default;
    explicit LogObserver (unsigned int);
    explicit LogObserver (const std::set<std::string>&);
    explicit LogObserver (const std::string&...);
    LogObserver (unsigned int, const std::set<std::string>&);
    LogObserver (unsigned int, const std::string&...);
    virtual ~LogObserver () = default;

    LogObserver (LogObserver&&) = default;
    LogObserver& operator = (LogObserver&&) = default;

    LogObserver (const LogObserver&) = default;
    LogObserver& operator = (const LogObserver&) = default;

    void notify (LogLevel, const std::string&, const std::string&);

    bool undefinedAllowed () const;
    void allowUndefined ();
    void blockUndefined ();
    bool verboseAllowed () const;
    void allowVerbose ();
    void blockVerbose ();
    bool noticeAllowed () const;
    void allowNotice ();
    void blockNotice ();
    bool warningAllowed () const;
    void allowWarning ();
    void blockWarning ();
    bool errorAllowed () const;
    void allowError ();
    void blockError ();

    bool contextAllowed (const std::string&...) const;
    void allowContext (const std::string&...);
    void blockContext (const std::string&...);
    bool allContextAllowed () const;
    void allowAllContext ();
    void restrictContext ();

protected:

    virtual void notifyCallback (LogLevel, const std::string&, const std::string&) = 0;

    unsigned int m_allowedLevelFlags {
        ALLOW_UNDEFINED |
        ALLOW_VERBOSE |
        ALLOW_NOTICE |
        ALLOW_WARNING |
        ALLOW_ERROR
    };

    std::set<std::string> m_allowedContexts;
    bool m_allContextsAllowed {true};

};
}

#endif

ConsoleLogObserver is a specialization that sends the messages to the console.

#ifndef H_CLASS_FILELOGOBSERVER
#define H_CLASS_FILELOGOBSERVER

#include <string>
#include <set>
#include <ctime>
#include <iomanip>
#include "../LogObserver/LogObserver.hpp"
#include "../File/File.hpp"

namespace tw {
class FileLogObserver : public LogObserver {

public:

    FileLogObserver () = default;
    explicit FileLogObserver (const std::string&);
    FileLogObserver (const std::string&, unsigned int);
    FileLogObserver (const std::string&, const std::set<std::string>&);
    FileLogObserver (const std::string&, const std::string&...);
    FileLogObserver (const std::string&, unsigned int, const std::set<std::string>&);
    FileLogObserver (const std::string&, unsigned int, const std::string&...);
    ~FileLogObserver () override = default;

    FileLogObserver (FileLogObserver&&) = default;
    FileLogObserver& operator = (FileLogObserver&&) = default;

    FileLogObserver (const FileLogObserver&) = default;
    FileLogObserver& operator = (const FileLogObserver&) = default;

    std::string getFilePath () const;
    bool setFilePath (const std::string&);

protected:

    virtual void notifyCallback (LogLevel, const std::string&, const std::string&) override;

    virtual std::string formatMessage (LogLevel, const std::string&, const std::string&);

    File m_file {"./log.txt", File::ENABLE_WRITE | File::ENABLE_APPEND};

};
}

#endif

FileLogObserver is a specialization that stores a file handle and writes log messages to that file.

And finally, FunctionLogObserver is a specialization that stores and manages a collection of function objects that receive the message (so in a way, it can do anything).

#ifndef H_CLASS_FUNCTIONLOGOBSERVER
#define H_CLASS_FUNCTIONLOGOBSERVER

#include <string>
#include <set>
#include <vector>
#include <functional>
#include <mutex>
#include "../LogObserver/LogObserver.hpp"

namespace tw {
class FunctionLogObserver : public LogObserver {

public:

    typedef std::function<void(LogLevel, const std::string&, const std::string&)> Callback;

    FunctionLogObserver () = default;
    explicit FunctionLogObserver (const std::vector<Callback>&);
    explicit FunctionLogObserver (const Callback&...);
    FunctionLogObserver (const std::vector<Callback>&, unsigned int);
    FunctionLogObserver (const Callback&, unsigned int);
    FunctionLogObserver (const std::vector<Callback>&, const std::set<std::string>&);
    FunctionLogObserver (const Callback&, const std::set<std::string>&);
    FunctionLogObserver (const std::vector<Callback>&, const std::string&...);
    FunctionLogObserver (const Callback&, const std::string&...);
    FunctionLogObserver (const std::vector<Callback>&, unsigned int, const std::set<std::string>&);
    FunctionLogObserver (const Callback&, unsigned int, const std::set<std::string>&);
    FunctionLogObserver (const std::vector<Callback>&, unsigned int, const std::string&...);
    FunctionLogObserver (const Callback&, unsigned int, const std::string&...);
    ~FunctionLogObserver () = default;

    FunctionLogObserver (FunctionLogObserver&&) = default;
    FunctionLogObserver& operator = (FunctionLogObserver&&) = default;

    FunctionLogObserver (const FunctionLogObserver&) = default;
    FunctionLogObserver& operator = (const FunctionLogObserver&) = default;

    const std::vector<Callback>& getCallbacks () const;
    void setCallbacks (const std::vector<Callback>&);
    void addCallback (const Callback&...);
    void clearCallbacks ();

protected:

    virtual void notifyCallback (LogLevel, const std::string&, const std::string&);

    std::mutex m_mutex;
    std::vector<Callback> m_callbacks;

};
}

#endif

If you’d like to read the implementation .cpp files, those are available in the git repo. They didn’t seem to be necessary to illustrate the design pattern. Also, this example code isn’t perfect, and I’m sure in short order I’ll find reason to modify it. At the time of writing, TimberWolf doesn’t have a stable API, so please don’t use this as documentation for it.

Categories
Programming Languages Software Development Web Development

JavaScript: Love, Hate, and Standardization

Recently, I’ve read a lot of hate toward JavaScript (which is easy to explain, see my previous post titled “Programming Language Wars“), and specifically there are a number of people who would request that browser vendors and the web standards organizations implement additional/alternative programming languages. This is a very naive request that I believe stems from lack of respect for standardization and the challenges facing browser development.

First of all, “JavaScript”, AKA ECMAScript AKA ECMA-262 AKA ISO/IEC 16262, exists primarily as a standardization, and that’s the important part here. Despite its origins, JavaScript has been around for decades, and likely will persist as such for decades, maybe even centuries to come. It was selected to be the core of the programming side of the standardized web. This standardization has been of particular benefit to making the web more or less completely compatible between a variety of browsers. JavaScript is defined in clearly written specifications, and as a result, each implementation, from Firefox’s Spidermonkey, to Chrome’s V8, to the dozens of other complete implementations, can be implemented separately and therefore be mutually independent from one another, while at the same time each supporting the same code so long as the developers making these products follow the standards correctly. This also affords freedom for competition to create distinct implementations instead of relying on a centralized one, which cultivates innovation.

There are many different languages that have a similar standardization and specification process, but this is not the only important characteristic of JavaScript. As a language designed for the web, it cannot make any presumptions of the architecture on which it will run. This means it would need to be provided as a scripting language (which JavaScript is), or a language of virtual machine bytecode (which WebAssembly is, which I’ll get into later). In addition to this, the language is supposed to be entirely forward compatible, so that code written a long time ago is capable of running on newer engines without modification. On some languages, this requirement can be, and is, safely ignored. Allowing things to be routinely deprecated and changed breaks old code. Often in these cases, the solution is simply to run the code on an outdated interpreter. This is not an option for browser vendors, however, whose releases already push several hundred megabytes in size, to say nothing of the nightmare of deciding which versions are still to be shipped, or the support nightmare of requiring end-users to manually choose their interpreter versions. These characteristics rule out a number of other possible languages.

Yet another issue with implementing other languages is the prevalence of JavaScript. Whether or not you like it, which is really not of concern here, JavaScript is arguably the most popular programming language in the world right now. Its forgiving nature cultivates ease of use for the beginner, largely due to being untyped and having automatic garbage collection. Additionally, the massive amount of development done due to the importance of the web has made JavaScript very performant. It also has a very C-like syntax, which is very popular and familiar to many developers.

Some people are hasty to criticize a language or library for catering to the needs of the less-experienced. These people usually complain about the loss of quality by making programming more accessible. But as is usually the case, ease of use does not preclude advanced functionality. JavaScript is a very mature language, and as such, it has a large user base that demands access to more advanced features and the ability to optimize performance more than an entry-level user requires.

And finally, and in my opinion, the biggest reason against implementing new languages for the browser is maintainability. As I write this, the Chromium JavaScript implementation, V8, has 1350 open bug reports. This is the number for a single programming language. If the web standards were to mandate the addition of another language, this would have to be implemented in yet another system of roughly equal complexity, and this would cripple the progress of development teams that even now are hard at work steadily advancing the single language implementation they have to maintain already.

All of these arguments aside, however, I fully understand the desire for developers to work in a language that suits their own style. I know the struggle of being forced to work with a programming language that I don’t like. So if I believe that, why do I still deny that other languages should be implemented in the web standards? First of all, I believe the benefit is greatly outweighed by the detrimental effect it would have on the speed of development for the open web standards. Second of all, if new languages are implemented, sooner or later, even those languages will come under fire for essentially the same reasons.

What we need isn’t to explicitly modify those standards to include languages, but to provide a common foundation on which new languages can be founded without requiring modification to the central standard itself. The simple answer to this is an intermediate language or virtual machine.

So far, JavaScript itself has acted as this intermediate language. Languages such as TypeScript, CoffeeScript, and Dart transpile into simplified JavaScript with boilerplate to implement missing features. Emscripten has even allowed the transpiling of languages such as C or C++ into JavaScript.

More recently, however, there is a new standard known as WebAssembly, which I believe solves this problem even better. One of the main purposes of WebAssembly was to provide an avenue for higher performance computing in a browser environment. One of the lowest levels of abstraction of an execution environment is assembly language. It encodes the primitive computational operations of the hardware itself, and has very few, if any, high level features. This lower level of abstraction also means that instructions written in it have little to no overhead. Due to the requirement for web languages to be architecture-neutral and to have some level of access control security, this had to be implemented in a virtual machine architecture that is conceptually close to a CPU architecture, but with a universal specification, and the ability to restrict the execution context to resources that it should have access to. This design is remarkably similar to the design of the Java virtual machine.

In the future, I suspect that many more programming languages will have either integrated or third-party backends that allow software to be compiled to run in this environment. This will require a fair amount of work to get this completed for each language, but I think it is the best way forward. With this method, any language could become a web-compatible language in the future, without needing to be integrated into the web standards, and without requiring extra work by the browser developers. By funneling all languages into the same intermediate language, it also ensures compatibility between them. And finally, because this method requires a compilation step, it means that if the programming language is updated and breaking changes implemented, previously compiled code will continue to function as normal until recompiled. The breaking changes may therefore still impact the developer, but not the end-user.

So to sum up: NO, new languages should not be integrated into web standards, but YES, developers of other languages should utilize transpiling and WebAssembly to make their languages available for web development. Other web-friendly programming languages should not be competing with JavaScript, they should rather see JavaScript and WebAssembly as their natural choice for an intermediate compilation target.