My last post, about a Distributed Application Model, was an attempt to separate one of several good ideas buried in Project MAD in hopes of making them more accessible to others. This post is similar; in many ways it is reframing what I said in the last time, with a specific focus on explaining the core mechanism and how it flavors everything else that is going on in the MAD OS and MAD/ADA system.
We are talking, today, about dynamic linking under Project MAD, or if you prefer, MAD/Libs. And to begin, let's talk about how this normally works, as I understand it.
Preface: Standard Runtime Linking
Programs (let's leave aside scripts for now) are normally written in a human-readable language and then translated into machine code in a process called compiling; frequently, there will be multiple, separate bits of machine code generated when you compile a program, as programmers like to split things up into logical segments. Because the machine code is still structurally similar to the layout of the human-readable program, you can still recognize that in the resulting machine code, for example, a particular function starts here, and a particular bit of data is there. An index of these facts about the machine code - where things are and what each of them is - is included with this compiled object.
When one of these compiled objects wants to reference another, you must necessarily be very precise and specific, in two parts: you must know what other object file contains the thing you want, and then know how to find that specific thing among all the other things inside of that file. During the process of compilation, it's easy to keep track of this all automatically; a mapping is made between an identifier and where exactly to find it, in both human-readable and machine code forms. Thus if you want to combine several objects together, you simply go through the object's dependencies, and find which file to look at, and when you combine the objects together, you ensure that the function call that was previously left as a dependency is translated into an actual function call within the final object. This process is known as linking.
There is a form of linking at compile-time which depends on code that the user themselves did not create (which is called a library); this process is known as static linking, and it is, ultimately, the same as it is when you link together objects that the user created, except that the library isn't something that you just created and so you have to know where it is. The process of specifying and finding these libraries is standard, and the details get into operating system and filesystem standards; common libraries are usually stored in standard places, and you only need to reference them unambiguously.
If, at compile time, the library you say you need cannot be found, the compiler makes a fuss and everything stops. Mostly, the list of these library files is stored in configuration files along with all the human-readable program source files, and the linker program just checks with the OS to see if they can find the files you want. And of course, if someone else wants to compile your program (assuming you share the source files), they can reference this list and go looking for exactly the libraries they need to compile the program, but assuming they have or get all those libraries, they will resolve all the dependencies your program has and come away with a finished, working executable.
A dynamic or run-time linking model alters this last bit. It allows you to create an executable that still has dependencies; the executable is considered “finished”, but when you go to run it, the process is interrupted until the system can find a copy of the libraries it depends on and link them in. Because this process happens on computers that the programmer doesn't control, if anything goes wrong at this stage, it can be a little harder to figure out and fix, but we generally have the process figured out nowadays. (In theory. Mostly. Kind of.)
Other than that, dynamic linking is only slightly different than static linking. Because these links are designed to be made when the program is loaded rather than having the canonical on-disk file altered, they are often done with function pointers - which treat the concept of where to find the function as data which can be modified, as opposed to a static linking model where these offsets may be metaphorically set in stone. Even then, though, the system must find and load the file containing those functions, before it's possible to update those function pointers so that they will work correctly for the program that depends on them.
There are a few added complications, but by and large, this is representative of the process that we call dynamic linking, and to summarize one last time, it requires you to keep track of identifiers (as originally written in human-readable language) and what file to look for them in, and it requires those files to provide a listing that translates the identifiers into pointers for functions and data which can be stuck into the program to allow it to run. Ultimately, this is what it takes for a program to call a compiled function stored in another file; to put it in a word, it's just recordkeeping.
Problems With the Extant Model
The model as we use it today assumes that linking is and must indefinitely remain highly specific. It simply does not do for someone to link any other object file in place of the requested one; it is assumed that you are referencing some complex bit of code that works on every client machine in exactly the same way as it worked on the developer's machine, and nothing else will do. This is not without reason; it's really problematic for a program to work differently on some random computer as it works for the programmer.
The problem is, because the linking process is very specific, operating systems and programming tools lack the features that would be necessary to facilitate generic linking. The concept does appear in scripting languages, where linking isn't a thing, and there are concepts in programming that do the same thing within a given project (using dynamic function and data tables, again), but it remains a presumption that everything will be done exactly the way the programmer originally intended - and again, this is not without reason.
There problem is, this non-generic structure is remarkably flimsy. Too many things are assumed and never tested; there is a lack of formal structure that allows you to describe things in unambiguous, but still flexible, ways. As a result, there are tons and tons of small, specific, things, many of which depend on other small specific things, and if any of those small specific things break, much larger things that you would not expect to be fragile may also break.
Anyone who uses Linux (and related systems) to any degree has encountered situations where they go to update their systems and receive a deluge of small updates. If one were to examine many of these updates, it can be alarming just how many are redundant; there are libraries that do the same or similar things but have different interfaces, libraries that do the same thing in different programming languages (especially scripts, which are wholly incompatible with one another), thin front-end executables that expose library functionality to shells (usually exposing only one specific library per executable), services that just sit around exposing a library's functionality, and libraries that compete with one another and may have some incentive to be incompatible.
What makes this system fragile is that none of these copies or similar functions can be easily swapped out. If a given library used by a given program becomes a problem for whatever reason, everything that depends on that specific library fails, because there is no easy way to transition from using that library to using an equivalent library. You can roll back to an older version… but that's only a patchwork, a temporary fix.
Why are they all incompatible? The easy answer is because nothing is standardized, and that's correct, but I will say it differently. There is no standard language for describing the dependencies of a program. Enumerating them, yes; any given executable will tell you that it depends on function X in library Y. But that enumeration of the dependencies does not describe anything.
So long as we cannot describe our dependencies, it is impossible to offer a generic alternative.
A Standard API Is A Generic Dependency
It's probably clear to existing readers of my blog that I have been describing for a bit, the idea of standardizing APIs. Anyone who is new will come to know this concept as the System API Directory, also known as the MAD/SAD subproject. But what specifically am I advocating for?
Suppose that your program wants to play a sound file; suppose that sound file is provided by the user, and you do not know in advance how the sound is encoded. When I talk about describing a dependency, I mean that your program wants to be able to say, “I want to link to a library that will play a sound, as it was stored in a file.” There are extant libraries that provide programmers a “one-stop shop” for various sound file formats, but depending on such a library is not the same as describing the intent of your program. You remain dependent on that one specific thing, which itself has many additional dependencies, some of which your own program will never use.
Just as your program cannot be linked with just any library, the library was not written to be used in just any arbitrary way. The library was designed to be used some specific way, and that specific way becomes the structure of your program. Once your program is compiled for one library, there is no going back; even if you had another library with identically named functions, it is impossible to know for certain that they are compatible. (Technically, yes, you could try to fool the linker, and perhaps even succeed - but it is not how the system is designed.)
When I talk about a standard API, I am talking about something that exists to guide the library programmer as much or more than it guides the applications programmer that will depend on it. It does more than simply telling the library programmer what names to call their functions, and the applications programmer what the function name to call is. It informs the structure of both projects; it describes when memory is allocated and when it is released, what guarantees can be made, what may or may not be taken for granted, and how errors should be handled. Only when both sides agree on these kind of terms can a specific library be replaced with a generic alternative.
The language of APIs was always meant to describe exactly these sorts of problems, but to date it has largely been left to creators to decide what's in them. And the point I'd like to make right now, is that until standards exist, libraries cannot be tested against and held to those standards. And so long as libraries cannot be held to standards, you cannot use them generically, only in the highly specific ways they were designed to be used.
When you look at the state of code libraries, you will see that they must, will, and have created their own ad hoc standards for almost every task. What is perhaps most distressing, however, is how commonly these ad hoc standards are literally the first thing that came to the creator's mind. Once the library has been written, and applications linked against it, it becomes painful to change the API and force all the applications using it to rewrite their own code. Even before it sees any kind of widespread usage, altering the API may mean making large structural changes to how the library was originally laid out. If there is no impetus to do so, it is most likely that the programmers never will, and whatever form the idea first had, it will continue to have indefinitely.
I choose to place the blame for the “fragility” of non-generic interfaces and linking schemes on this problem: too many have unique and bespoke interfaces, with no agreement and no impetus to agree on what those interfaces should look like. Programs that depend on some arbitrary set of libraries may find that in different places in their code, the same task is performed by different sub-dependencies, each working slightly differently, and those differences may require different structure around each function call.
I do not intend to disparage individuality in programming methodology; having multiple ways of doing a similar task is valuable, and arguably critical for long-term success. However, having these multiple methods be fundamentally incompatible, especially where it need not be so, is problematic. Indeed, I have long thought that one of the benefits of this kind of generic linking is giving users (not merely programmers) the ability to swap in a more optimized version of some algorithm when it is discovered, or to swap in a more transparent version if debugging or monitoring a program. (There is a security argument to be had here at another time.)
I am also not necessarily suggesting that there be one ultimate and irrevocable interface for some given class of problems moving forward. Rather, I am talking about deciding upon some minimum reasonable standard which defines the very concept of utilizing a technique, algorithm, process, resource, or device. For sounds, for example, this minimum standard may include playing the file, pausing and stopping, playing repeatedly, getting or setting the current playback position, and changing the volume. This list by no means describes everything you may want to do with a sound file, but it is a fair minimum standard (and I would gladly listen to arguments for expanding or shrinking it).
Of course, going beyond the minimum is a matter which brings its own concerns.
Expanding the Language of APIs
It was never my intent, and it remains not my intent, to claim to know how best to organize APIs, or to lay down firm standards without discussion. As such I hope you will take the following section as informal, tentative, and conceptual.
I have talked in the past about the MAD System API Directory as containing a hierarchical tree of types, from most generic to most specific, and to this concept I still hold. The nature of this kind of tree makes it technically possible that all possible interfaces can be categorized into rough categories, but this alone may not prove good enough for the task of describing a large, diverse, and robust system.
Originally, I envisioned that the first few layers of this tree were broad categorizations, and once you pass a certain point, then programmers are free to create various child standards with whatever additions to the parent standard they wish to add. This has the problem, however, of not providing granular extensions to an interface; part of the intent behind the broad categorization is to establish simple baseline APIs that allow programmers to avoid interacting with unused features, for example when reading documentation or when experimenting with the functions of a library.
If you don't have some method for granular extensions, then you are obliged to create complex child standards almost immediately. For audio files, for example, one program might have a specific need to pitch-shift an audio file (for example, to tune a musical instrument sample to different pitches), but otherwise, you might only use basic capabilities of an audio playback library. Another program might need to cleanly loop audio, potentially with defined entry and exit audio segments (for example background music, with an intro and coda). If these functions are not part of the baseline, in a child-only model, it stands to reason that someone will immediately create a “everything and the kitchen sink” child API that includes both of these features and tons more. Worse, two similar but unequal child APIs may be mostly but not completely compatible, and there is no language to describe their intersection or extremities.
Although my experience with API design is minimal, I cannot help but think that the language of API design itself needs to be extended. The above problem, for example, seems like it is begging for a concept of “features” which describe granular additions to an API; a child API may provide, or a program may require, some set of those features, but more to the point, the baseline API standard can be read and understood absent technical discussion of advanced features. Whether or not features in this sense are the right way to accomplish this, I believe that finding some solution to this problem is laudable.
I have other similar thoughts - enough that it would complicate this section and blog post. Some are already discussed elsewhere on this site, others will be described at some point.
But to summarize this for right now, remember that we are talking about making libraries into something predictable and testable. That's all that's really required for generic linking; as long as you know what will happen when your program calls a function, then you can move away from depending only and very specifically on the same file from the same vendor as you originally designed the program to use.
But for all that I've talked so far about the problems with what we have, I haven't talked much (in this post at least) about the benefits of a modular, genericized library structure.
I Did Call It MADLibs
I talked in the last post about making devices accessible using inter-process communication and remote procedure calls. Without rehashing (or if I'm being honest, rereading) that whole argument, I'd like to point out that the idea of generic linking fits directly into the idea that devices (in the OS sense, which may include things like files) have inherent capabilities.
Generic linking suggests that anyone writing library code knows how to expose functionality in such a way that if a program depends on some specific capability (in the API sense), it can know whether your library provides that specific capability. Devices necessarily have some kind of driver, even boring devices like files; the driver describes the capabilities of that device. Thus it stands to reason that a library should be able to make the claim, that it provides a device specific capability, when paired with some device of that type.
At this point, we have returned to this post about assigning types to every filesystem object. And while that kinda is the point, I'd like to phrase it a certain way right now:
Libraries which provide device-specific capabilities are verbs that act upon a noun. Any given program may start with a specific verb and allow the users to fill in a noun, but you can also create a list of all device-specific capabilities that anyone claims to offer for a specific class of device. In other words, a highly generic program such as a user shell (file browser, etc) can start with a noun and allow the user to select any appropriate verb, provided by any library that is available on the system. The user, of course, has a lot of files and devices available to them; they have a lot of nouns, each with a comprehensive list of verbs.
I said in my last post that “it may surprise people who don't look at how OSes work that an average programmer probably doesn't know how to get a list of all devices present on a given machine, can't tell what a random device is capable of, and can't figure out how to control that device.” This is the culmination of that indictment of modern computing: A Madlib-style system, mine or otherwise, that allows you to cultivate a list of verbs that work with nouns and nouns that work with verbs is absolutely possible, it just doesn't exist in systems as we have them today. Certainly not in the generic case, where you expect the same methodology to work as well with services and hardware devices as it does with documents and media files; the “file associations” structure that modern operating systems use is primitive in comparison.
It is also important that the Madlib-style system provides a rigorous way for the system itself to know how to provide a given capability for a given device. Remember that Project MAD is distributed; if you want to consume a capability on a remote piece of hardware, you need to know what, if anything, you need to bring with you when you send a program agent out to consume that capability, and you need to know what to expect when you get there. If for example a remote node has a camera, and some nearby tertiary node contains a library that knows can provide a specific capability for that camera, you may need to copy the library over to the camera host and load/run it, in order to get the required capability.
It is important to consider this, because the library you're talking about may have multiple dependencies. The camera capability you want to consume, for example, may also require an AI accelerator chip, which may not be present on the camera node, but it may exist in the larger system. If you want to consume this AI analysis of the camera feed, a separate setup process must first begin which connects the camera to the AI accelerator, and then your program to the output of the AI accelerator.
Under MAD, this scenario makes sense. It is reasonable to expect this stack of programs, libraries, and drivers to deploy automatically when you ask to consume what appears to be merely the capability of the camera. It makes sense that you can simply use a verb on a noun.
If someone else finds a way to do the same… I welcome it. Ultimately, I really just want computers to be better. In the meantime... I hope this sparks at least some thought on the subject.