Skip to content

Latest commit

 

History

History
131 lines (76 loc) · 43.9 KB

README.adoc

File metadata and controls

131 lines (76 loc) · 43.9 KB

WAHE

WAHE is the WebAssembly Host Environment, an environment that hosts modules and makes them work together. The idea is to have WebAssembly modules that can work forever on any platform, that communicate entirely through textual messages that can be manipulated by other modules, that can be plugged into each other so the output of one can become the input of another, to dramatically lighten the burden of developers, to give control to users and thus blur the line between developer and user, and to end the obsolescence of software, not just by having WebAssembly modules always be executable but also by enabling anyone to modify the messages that go in and out of a module to make them forever adaptable to new circumstances.

This to me is the logical conclusion of watching very old video game ROMs effectively turn into universal programs that will never break while modern computer programs and plugins age about as well as a banana. Today you can create a NES ROM that would run perfectly well on an emulator that hasn’t been updated since the 1990s and that will still work centuries from now because NES emulators will still run your ROM fine, so why not apply that concept in a more powerful way? And while we’re at it let’s also do away with the concept of programs that forces programmers to control everything directly and gives users or other programmers no opportunity to control what happens (if a program creates a window, goes full screen, tries to open a file from the A: drive, opens a dialog, you can’t stop or modify that unless the program’s creator goes out of his way to give you a choice).

As it is this is a C library that relies on Wasmtime, although the dependency on Wasmtime can be disabled so that only native modules would work, and using wasm2c WebAssembly modules can be turned into native modules so that anyone can convert WebAssembly modules and run them on any platform and with better performance.

Concept

There are a few concepts that define WAHE:

  • A host can take any form and directly handle certain features or leave them up to modules. The WAHE library is meant to be fairly minimalistic and the WAHE code in each host is meant to be very small. The concept here is that new functionality should come from modules, not the WAHE library nor the host, so a host should ideally only need to be updated for reasons not related to WAHE, and since much of the OS integration (creating windows, sound input and output, OS dialogs…​) should be done through native modules this should make hosts be able to stay unchanged for a long time once WAHE reaches a mature stage. It’s important for anyone who creates a host to adopt this concept to avoid divergence and incompatibility between hosts. Native modules should drive the development of functionality, not hosts, and the WAHE library minimally so.

  • Modules are made of callbacks, they take textual messages as input, return textual messages, emit textual commands to the host (which can route them to other modules) and expect textual responses. They can also synchronise data through mutex-protected named buffers in host memory. All module callback functions have one string argument and return a pointer to a string, for instance: char *module_draw(char *input_msg).

  • Modules are arranged in a setup described in a .wahe text file which refers to modules to load, what commands to send to each module at initialisation, execution chains which contain the order of execution of specific functions from a module, and connections describing which outputs are sent to which inputs. This means that the basic concept of execution is to run one callback function after the other, typically with the output message of a function becoming the input message of the next one.

  • A chain described in a .wahe file as a series of execution orders and connections might be run directly by the host or from a module through a command that names the chain. In the latter case for the calling module the command only returns after going through all the execution orders in the named chain and receives as return message the last message from the last execution order. This allows things such as having a native module that at initialisation starts running a chain in a loop (for instance a keyboard-mouse-graphics loop in a window) or registers a callback, such as an audio callback that runs the audio execution chain.

  • Modules can register textual commands to the host so that other modules will be able to run these commands. Using this a module can take over from another module for a specific command so that it would be the one who gets to execute that command.

  • All those text messages to and from a module are meant to be interceptable and modifiable by other modules, so a module doesn’t need to directly communicate well with other modules or the host, there’s no strict need for a module to respect any standard, another simple module can be created just for translating messages between a module and the rest, which is an essential part of being able to keep experience with an old module up-to-date.

  • Modules are either WebAssembly or native (like .dll files on Windows). WebAssembly modules are meant to be platform-agnostic and have the potential to work well forever without ever becoming outdated, native modules are intended to simply provide access to certain features.

  • WebAssembly modules should send commands that aren’t too specific to the current circumstances (such as the platform it expects to run on, the APIs or libraries it expects will be used), rather the commands should express an intention that will always be valid, and likewise native modules should make it their job to take commands that express such an intention and turn it into something adapted to the specific circumstances it’s made for.

  • WebAssembly modules have their own address space, they can’t directly access data outside of it, however the host can directly access data in every module’s memory so modules can order the host to copy buffers from one module’s memory to their own. This makes it possible for module B to receive a message for module A describing a buffer in module A’s memory and have the host copy it to module B’s memory so it can be used. Modules receive a 60-character identifier from the host so that they can self-identify when writing a message that describes data in their memory.

  • There’s only one function that a module should use to get anything at all from outside of itself: wahe_run_command(), it takes only one argument, a string in the module’s address space, and it returns a string dynamically allocated in by the module’s malloc() that should be free()'d by the module after parsing. In conventional terms this is the entire API, all functionality comes as textual commands sent through this function.

  • Other than its callbacks, a module also exposes its malloc(), realloc() and free() to the host so that the host can create and modify buffers in modules.

  • There are no hard standards, no standard pixel format, no standard audio sample format, no standard way of describing pointers, nothing like that. If a module decides to output sound in 16-bit integers and another module decides to output sound in piecewise polynomials that’s okay, it’s another module’s problem to convert them to whatever format the native audio I/O module expects. There are soft standards that come from imitating what is already done to minimise friction, and these can organically emerge.

  • Modules are meant to be self-documenting, because if you find an obscure module from decades ago there’s a good chance that you won’t find any information about it that isn’t contained inside the module itself, and developers tend to not like writing and maintaining separate documentation. This self-documentation is achieved by having modules respond to a Documentation command in module_message_input() by returning a string literal that details the functionality of each exposed callback function, what they take from their input message, what they return in their output message, what NULL messages mean to them, and details about the syntax and usage of each command they can take. This documentation can even be read by opening a module in a text editor and looking for Documentation.

Philosophy

The philosophy that drives WAHE is more important than its technical aspects. It’s important to understand what’s wrong with the concept of programs and the burdens it imposes, the impracticality of open-source software, the problem with libraries, and how we can do everything much better and more efficiently than we’ve done so far.

The problem with programs

The concept of programs is primitive and essential, a program is what would tell early programmable computers what to do, it took entire control of what the computer did, it’s how anything would happen. Then came the operating system, itself a set of programs that would get between any other program and the computer, it’s there to control when programs get to execute their code, to organise memory and present it to programs in an isolated and abstracted way and to provide access to all the computer’s features and hardware without each program having to worry about how to do that directly. Things have worked like this since the 1960s, and conceptually nothing has changed since then, we’ve been limping along with the same primitive concept for all this time when we should have gone further.

The problem with programs is the following. Control is given only to the people who create a program and people who create the operating system. To give any form of control to anyone else, the creators of the program and the operating system have to go out of their way to add features so that users can have control. For instance, for a program to emit sounds, the program has to use functions provided by the operating system to decide which of the audio APIs are available, which API should be used, which audio output device should be used, which sample rate, bit depth, buffer size and how many channels should be used. Not only does the creator of a program have the burden of trying to make this work well out of the box for any machine on any operating system that he wishes to support, he then also has to add a way for the end user to control those parameters, to select the API by themselves, the device and all the other parameters, because what the programmer selected as defaults might not be suitable to all users.

Even though it’s clearly a very significant burden for programmers and one that gets them lots of emails from users complaining that sound doesn’t work right, it’s hard to see how there could be a better way. Ancient video games once again show us the way: just as the original Pacman programmers never had to worry about selecting an audio output device on Windows XP because that’s the emulator’s problem, a module’s programmer doesn’t have to worry about this either, it’s not their module’s problem, in fact it’s not the module’s host’s problem either, it’s a native module’s problem, the native audio module that takes care of setting up everything and then running the audio execution chain in a thread and sending the result to the OS. And if a module presents the sound it emits in a way that isn’t directly usable by the native audio module, translating what a module emits into usable sound data is yet another module’s problem. So if there’s any problem with the sound output, the end user deals with it using the native audio module which would probably give it all the options needed to fix the problem, and if the native audio module is inadequate or doesn’t exist for the user’s system, the user himself can take matters in his hands and create the native audio module that will work for him, which he can then share with others who might need it.

This way the burden is totally removed for a module’s programmer, their only concern is how to calculate samples, not what happens to them, the maintainers of the host don’t have to worry about this either, it is only the concern of whoever chooses to create a native audio module, and ultimately the end user is given absolute control, without anyone else having to worry about how to give such control. Additionally the motivation for creating a native audio module comes from a personal need for such a module which is the most powerful motivator and gives the quickest results, and the desire to share such a module comes naturally to anyone who felt they did a good job, so with such a system any missing or buggy functionality can be fixed quickly, without even contacting anyone, even without open source, even without any other program being available to do anything, everything can be solved by the end user.

Audio output is a good example, but this applies to anything. A module programmer doesn’t have to worry about creating a window and how to display things onto that window using a library or an operating system’s API, all he has to do is output a message that describes graphical data, which could be a pixel buffer or even some drawing instructions, anything else is not his module’s problem, and details such as how that data should be shown in a window or on the screen can be in the configuration, which can be set in the WAHE configuration file, and this can be done by the original module’s programmer as well as the end user.

It’s easy to see why that would be beneficial, as the desirable way to show graphics on screens evolves over time and systems. For instance you can often still run video games from the 1990s and 2000s on Windows, but not without them making a big and very annoying mess first, because the thing to do for games back then was to pick a default resolution, something very small by modern standards, force the whole screen to switch to that resolution, they would also try to switch to a lower colour bit depth, and while this worked well back then this is very much undesirable behaviour now. And the problem is always the same, programs talk directly to the operating system to do what their programmers thought was best, and you have no control over this that either the program or operating system doesn’t give you, you can’t get in between the two and stop the program from doing something nor stop the OS from allowing it. Once again ancient video games show the way, their programmers had no idea that what they programmed would ever run on a machine other than the very one they programmed for, this is all the emulator’s problem, and the emulator can handle it all in a suitable and modern fashion.

And once more this alleviates another burden for module programmers, their only problem is how to generate graphical data, not what will be done with it, their task is purified, cut down to the essential of what is truly unique to what they’re creating. They don’t need APIs or libraries to display anything on screen, they simply generate the graphical information and print a text message that contains all the information to describe it. This makes it much easier for beginners to get their first pixels on screen, they just have to fill up a buffer with pixels in whatever format they choose, print the essential information like the buffer’s address, the resolution and format to a message, two arrays and one sprintf and that’s it, job done.

A philosophy of minimal burden and limited scopes

Imagine that you want to create a program that reads video files. You have to figure out how to use the FFmpeg library to decode all the data in video files, how to create a window, how to display images to it, how to make it go fullscreen, how to output sound, how to have a dialog to open a file, you create an interface, a play/pause button, a timeline so you can jump in time, keyboard shortcuts, a volume control, buttons to jump to other files, a playlist, a preference panel so users can select an audio output device. You can’t just know the FFmpeg library well, you have to also be good at all those other things. Then you release it, and besides all the bugs, users have many problems. They tell you about those problems because you’re the only one who can do anything about them, you’re the programmer, only you can control what’s happening, even if you make your program open source probably no one is going to dive into your code and change things for you.

"The volume control sucks, it’s too sensitive at the bottom and it needs to go beyond 100% when it’s too quiet but also not get too loud by accident", so you have to make a better volume control.
"No subtitle support? Come on!", so you have to both figure out how to read subtitles and how to display them on screen using a library.
"Can you add an equalizer? Can you add VST support like Foobar2000 has?" so you learn how to implement an equaliser and you say no to VST hosting.
"The subtitles get out of sync sometimes, I need a way to synchronise them manually" so you have to add some controls to do that.
"The image is too dark sometimes, it would be nice if we could make it brighter in dark scenes", so you consider working on that.
"I’d like subtitles to go over the black bar at the bottom and not over the video", so you look into it and realise that you render subtitles directly on the video frame and as it is you can’t directly draw over the black bars, another headache for you.
"Is it possible to make it detect when I insert a DVD or BluRay and play it automatically?" so you add that on your list of things to maybe look into in a few years.
"Is there an Apple Silicon build? Does it work on macOS 19.4.1? I can’t open it, it gives me this message."

It never ever ends, the emails will only end when your program falls into oblivion, until then people will always ask you to do something because you’re the only one who has control, and you’re the one who deals with how your program works on each system.

Here’s how it would work if you made a module that plays video files instead. To be exact there would be at least two modules, one that does the hard work of decoding files into usable video and audio data, and other modules to do everything else. The first one, the decoding module, would most likely be a native module, however its scope would be so limited that the main difficulty in making it work on all platforms would be figuring out how to compile the FFmpeg library statically and hardly anything else.

So for creating this native decoding module you’d need to know how to use the FFmpeg library, in fact it’s basically the only thing you need to know. So you work on the core of decoding videos with the library, how to decode image frames, how to decode sound, how to jump in time, optionally how to decode subtitle text, all those things which you should already know if you’re proficient with the FFmpeg library, and then, this is the crucial part, you make every feature available exclusively through text commands received from the module_message_input() function. So in that function you parse a command named "Open file" which tells you the path to the video file to load, maybe a command asking for information to which you respond by giving all the relevant information available in a text message, you might even want to include all the subtitle data in one block, "Play", "Pause" and "Close file" commands, a "Jump" command to jump to a given time or time offset and that’s about all the commands you really need. All that’s left is outputting audio data in the audio function and frames in the module_draw() function. In the message returned by the draw function you not only print the essential information that describes the image data such as its memory location, resolution and pixel format, which by the way should probably be the raw format you decoded such as planar YUV, you don’t need to convert it, this might be a waste of CPU time and also not your problem, and in that message you should also add the timestamp of the current frame, you can also add any text from the subtitles so that another module can make use of it, and there you have it, the module is done, it’s over, there’s no feature to ever add, only updating the FFmpeg library and fixing how it breaks its API on a regular basis and occasionally updating the builds for each platform which can also be done by other people if the module is open source, and why wouldn’t it be.

The graphical interface is another module’s problem, converting the image format to something that can be displayed is another module’s problem, how to pick a file isn’t your problem, keyboard shortcuts can be translated into commands by another module, there’s already a module for volume control that does a better job than you could have that takes your module’s audio output and modulates it so you don’t need to worry about volume at all, it’s not your problem, displaying subtitles is not your problem, you already provided all the information for another module to overlay subtitles on the raw frames you provide, upscaling, cropping, padding, stretching, overlaying, sound equalising, image processing is not your problem, detecting discs is not your problem. All these other modules can deal with the messages that your module emits, the way those messages are written can become a de facto standard, and they all can send command messages to your module to control what it does. You give all the control for what happens inside your limited scope module to other modules, and because of this your burden is limited to a strict minimum and no one will send you emails about all those things that shouldn’t be your problem. Even if your module is closed source and you walk away from it that’s okay, someone else can create another module that takes all the same commands and gives the same outputs, and everybody can replace your outdated module with the new drop-in replacement. And so nothing remains of your work except for the standard you set for how a video decoding module should communicate.

The philosophy of eternal modules

But that’s for a native module, native modules are born to die with the libraries they rely on and the platforms they’re made for. The other modules on the other hand have a chance not only to be eternally runnable but also eternally usable. Just as centuries from now people on whatever machine they’ll have will still play Super Mario Bros using the exact same binary data that was written into the original cartridges, so will people always be able to use our WebAssembly modules, provided that they can be bothered to maintain something that can interpret them. But how can they be truly usable if everything changes? It’s actually simple, the answer is message translation.

Imagine that a bunch of ancient modules are discovered. Their functions could be made to run, but just like ancient video games they need to be emulated. So a module to emulate them is made. Then we look at the commands that their functions take, the messages and the commands that they emit, and not only is it nothing like any of our modern standards but it’s in another language entirely. What can we do about it? A single module that translates all messages and commands going in and out of the ancient modules will do. Do their draw functions use flowery language to describe brush strokes? We can interpret them and render them into pixels. Do their sound functions use weird characters to describe musical notes to be played by specific physical instruments? We can synthesise that. Do they try to send the host commands such as "Tell me the twenty-third word inscribed on the reverse of Tablet Ksi-Omicron"? We can intercept such commands and give it back what it expects. And then with all this emulation and translation the ancient modules work just like modern ones and are good to be used again.

It sounds like by that logic anything can be made to run. But for programs, an entire machine emulator with the full OS would need to run, and while you could have some success by transferring inputs and outputs (such as feeding mouse, keyboard, audio and MIDI inputs to the emulated OS and video and audio outputs out) you would be very limited in what could be achieved, for instance you might not be able to load files from outside the emulated machine, you wouldn’t be able to transmit information from a controller into something the emulated OS could use, you wouldn’t be able to control much of anything in the emulated program with commands. The program would be limited by its own direct control over everything and by its operating system’s limited communication abilities, whereas a module would rely on being given commands and having its functions running as requested.

There’s a more subtle but crucial problem with old programs that old modules can sidestep to stay relevant. Imagine a video editing program from the year 1998. It has all the features a video editor needs, just one problem, it can only import and export video file formats available back then, so that even if you can still launch the program even without an emulator, even if it runs as well as it ever ran, it is now useless as it cannot use the videos you’d want to edit and it cannot produce video files that you’d want to use. The problem is that the program necessarily has its own way to decode and encode video files directly, so nothing can be changed about this. Not only this, but there’s no way for this program to make use of anything but its own antiquated plugins.

Imagine on the other hand a WebAssembly video editing module that is equally old. It’s a WebAssembly module, so it doesn’t handle files directly, for this it has to emit commands that other modules will take care of. So instead of running its own code for decoding or encoding the video formats common in its time, it emits generic commands such as "Load video file <path>" and "Jump to 00:19:02.35". Because it relies on other modules to figure out how to do this and deliver the results, it is immune to becoming outdated, it does so by expressing what I call eternally valid intent.

The concept of eternally valid intent

When we write programs we write code with the intent of achieving something specific. But we don’t actually write what we are trying to do, instead we write the code that will currently do what we want to do. We don’t write "Load video file <path>", we write codec = avcodec_find_decoder(CODEC_ID_MPEG1VIDEO); c = avcodec_alloc_context(); avcodec_open(c, codec); …​ len = avcodec_decode_video2(c, picture, &got_picture, &avpkt);, and the problem with this is twofold: it doesn’t clearly express what we want to do, and the code itself is already completely obsolete, it won’t compile nor do anything, the library still exists but the API changed completely like 4 different times in the years since, it would have to be entirely rewritten and then it assuredly would break again within the next 5 to 10 years.

So that code we write to load a video file is bound to break, it doesn’t stand a chance to survive the decades let alone the centuries. But if instead we emit the command "Load video file <path>" we emit the purest expression of intent, even if <path> might turn out to not always be an actual file or even an actual video, we express something that can easily be interpreted, by another module that may not yet exist, into an action: that module wants us to load this thing at <path> so that then it can take the decoded data. In other words such commands will always make sense, they tell other modules what to do without telling them anything specific about how to do it, and if in the future they need to be translated it wouldn’t be too hard to do so.

So there is a crucial separation in our modules: the algorithms that will always work in isolation from the outside, for instance how to draw the pixels of our interface using our own algorithms, data and logic, will be written as normal code, whereas what deals with outside information or execution flow, such as dealing with files, time, threading, network and devices will be dealt with using text commands and messages, so that what they’re meant to do can always be understood, translated and used. The hypothetical ancient module that does its graphics by describing brush strokes expresses something that can forever be understood and used, whereas the mid-2010s program that uses AMD’s Mantle API expressed nothing that anyone could possibly have used just a few years later.

One-way collaboration, the most potent kind of collaboration

The concept of open source programs fails in two important ways: diving into other people’s code to achieve the change you have in mind will most likely only make you achieve despair, and even if you achieve what you want on your fork, then what, do you do a pull request to make the creators of the program accept your changes? This too might make you only achieve despair. So then what, do you keep your fork, but the original project keeps getting updated so you constantly have to update your fork and deal with the conflicts between your changes and their subsequent changes? People prefer to tell the developers what needs to change and leave it up to them, so there’s effectively not much benefit to the code being open source.

Now imagine this situation: there’s a module out there, it’s not open source, in fact people pay $50 to have it, it even checks for a licence file every time it runs. Its core, the part that does all the fancy computations, is brilliant, no one else could do anything like it, but its interface really leaves to be desired, it’s barely usable, it’s clear that its creator is better at some things than others.

Excited by the idea of seeing something better, you observe all the text messages that go in and come out of the core until you know everything about how it works. Then you make your own module that is just a graphical interface that communicates back and forth with the original core, you make it check its own licence file and sell it online for $30. Users of the original module don’t mind shelling out a bit of extra money for a much superior experience, the original creator doesn’t even lose any sales and in fact he can use this opportunity to give up on his inferior interface and instead focus on what he really wants. Everyone wins, and yet at no point did the original creator coordinate with the second one, it only went one way, with everything being in the hands of the second creator. The process could go further with the first creator adding commands unilaterally and then it being up to the second creator to make use of them in his interface. Again no actual coordination needed, everyone just does whatever they see fit.

Organic emergence of standards

One-way collaboration also applies to creating a standard of communication between modules, as outlined earlier with the example of the video player. If you create a module that does something novel, you will create a set of commands and messages to control that module and a message format for its outputs. If you’re creating a module that interacts with a module that already exists, you will make your module communicate with that module in the way that that module communicates, you will send it the commands it understands and you will interpret the messages it sends back. By doing so you make it more likely that anyone who wants to create a module that serves the same functionality as that original module will also understand the same commands and emit messages in the same way, thus strengthening the position of the original module’s mode of communication as a de facto standard. If this newer module adds to this standard by adding new commands it can understand, it might effectively add to this de facto standard, even though no one gathered around to agree on the definition of a standard. Instead by an organic process, module creators would effectively unilaterally contribute to the emergence of de facto standards of communication, validated in parts by the unilateral adoption by others and invalidated in other parts by the lack of interest of others.

Code organisation and the failure of libraries

Programs fail in another important way. Being in control of what they do and how they do it from the moment they’re started invites anarchy in code, programmers organise the code of their programs however they see fit, and the result is usually both ugly and chaotic. A common sign of this anarchy is vast main() functions that do way too much in their body. As a result it’s hard to do anything with such code bases other than just let them run as programs, it’s hard to refactor them into a more library-like structure that would enable their use as something other than programs.

But even libraries, while they usually have a superior more sensible code structure, fail to be as good as they could be. The problem is that people who make libraries seem to hate simplicity. Instead of offering a functionality as a simple function they’d rather make you initialise three different structures and then create a loop to process data when most people just want something that could be done in one go. As a result when you want to do anything with a library it usually takes more than a day and you must do quite a bit of research, diving into documentation and example code.

Once again modules offer a superior alternative, somewhat as plugins (think VST/Audio Units) have done for a long time. By forcing the organisation of functionality into callbacks with a clear theme the code is less anarchic and more reusable as something akin to a library. The insistence on using human-readable text commands and messages which form a desirable information bottleneck as well as using callbacks for processing also forces a focus on simplicity: you’re not going to load a video by calling a dozen different library functions and storing internal information in 4 different structures in another module, instead you need to rely on a minimum of text commands and to process data through callbacks. For instance you could encode a video by sending a text command to the video-encoding module to start encoding a video file with a few parameters, and then feed it the images and audio to encode by sending the output messages of your image and audio callbacks as input messages to the encoding module’s image processing and audio callbacks, and then send another text command to end the encoding and close the file. Two commands would do, whereas my code that uses the FFmpeg library to encode video takes 350 lines.

Perfection and influence within a beginner’s reach

As outlined earlier, making a good program for even a simple task is hard, making a program that doesn’t have a long list of issues to fix and features to implement seems practically impossible, even to a wizard of code who can do it all or to a large team of highly competent developers at a trillion dollar company who seemingly can’t even produce a program that doesn’t crash regularly, even after a few decades of development. What hope is there then for someone who doesn’t master every possible aspect of creating a good program or isn’t part of a team, what can the lone beginner with basic programming skills do? Not much of value with programs, however with modules they have the opportunity to create simple modules that do one task perfectly well and that, due to their perfection and completeness, can only be adopted by anyone who needs such functionality. How attainable such perfection and influence would be can hardly be overstated. Imagine that you feel the need for a command that can access data in a file at any position, or maybe you need an ICMP ping command, maybe you need a command to display a dialog box, or maybe you need to implement a command that is implemented for other operating systems but not yours. Whichever you choose, it’s always the same process, you look up how it’s supposed to be done, you adapt code that you find, put it in a new module to be executed when receiving the text command, make the module register the command as a response to the Command registration command, describe the command and its syntax as a response to the Documentation command, compile the module, release it, and that’s it, job done, perfection achieved. When your module that does a simple thing perfectly is done and released, why wouldn’t everyone who needs it not use it? No one has any interest in ignoring a module that already does the thing they need. This way an entire sophisticated ecosystem can be built, even by people with the most basic programming skills. Suddenly there isn’t a high bar for contributing something valuable anymore, basic skills can be perfectly sufficient.

But this isn’t all, the potential for influence isn’t limited to creating something new and having other module developers rely on it, there is potential for a more instantaneous and frictionless kind of influence. Imagine that you’ve always hated open/save file dialogs, you hate all the ones provided by Windows, macOS, GNOME, KDE, the even worse custom ones that some developers insist on using, you think they’re unwieldy, unpleasant, have failed to evolve beyond providing the bare minimum of functionality, more importantly, you have a better idea. Traditionally the best you could do would be to implement your file dialog in a library and hope that other developers adopt it. But no matter what you do very few developers can be swayed, so no matter how great your library is its adoption is negligible, which defeats your dream of a world with better file dialogs, regardless of the merits of your work, it’s simply impossible. Now with modules you implement your superior file dialog in a module, make it register the Open file dialog and Save file dialog commands, and this is the crucial point, these are the commands already implemented in the "OS basics" modules that all the other modules already use, your implementation would take over, so anyone by adding your module to their list of modules that are always used (some modules in an editable list like "OS basics" or audio output modules are always initialised when a WAHE host starts so they can provide certain commands like Open file dialog to any module and some functionality like the ability to run audio execution chains to all setups) will have all file dialogs in all circumstances be handled by your module, so not only can you make your own world a better place by making all modules use your dialog right away, but on top of it any simple user can do the same, simply by adding your module to their environment. From the moment you let others know about your module some will test it, and there’s ultimately no limit to how many users might adopt it, without any other developer ever needing to do anything.

Total determinism, replayability and bug reproducibility

According to the WAHE approach, WebAssembly modules only have two sources of information: the information contained in their own data, and the information that the host places into their memory in the form of textual messages and buffers of data. The latter come either as input messages provided to callback functions or as responses to commands issued by the module to the host. So a host providing the same data in the same sequence as before will result in a module being in the same state. This has interesting implications for bug reproducibility. Let’s say you’re struggling with a rare GUI bug where for some reason a window stops responding to clicks entirely. You can’t figure out why this happens nor how to reproduce the problem reliably. Fixing a bug that can hardly be reproduced is very difficult, but now let’s say your host records all the mouse and keyboard inputs from the beginning of the execution of the module, then you can make it reproduce this sequence of messages after the right number of executions of each module callback and obtain the same result. You can modify the module to add debugging information that will help you identify the problem, and then you can try fixes and validate them by replaying the messages that originally revealed the bug.

By reducing code to functions called by a host and sources of information to messages provided at well-determined occasions we gain precious advantages over programs and plugins that control their flow of execution and directly get information from the OS. The inputs to all functions as well as the responses to commands all being single text messages (if not null pointers) makes them easy to record and replay. This replayability also implies that the state of a module can be restored not only by directly restoring the state of its memory (which is possible but not efficient nor flexible) but also by replying a sequence of function calls with recorded input messages.

Modules define external functionality

There’s a crucial and essential conceptual difference between programs and WAHE modules that gives the latter their eternal and universal viability. Programs directly access functionality offered by the operating system, which is a main reason why they’re not very portable and end up breaking. For instance you use GetOpenFileNameW() on Windows or [NSOpenPanel openPanel] on macOS, this makes your program only work with one or the other, and when Apple decides to stop using NSOpenPanel then your program stops working. Using text commands instead of function calls alone doesn’t change this, it could well only add a pointless layer between the module and the operating system, and on its own giving the commands more common sense names like Open file dialog wouldn’t change that either.

The crucial difference is that your WAHE module doesn’t care what is available, it’s not its problem, it sends a text command and either gets a text response or a NULL pointer. If the command is unknown to the host, the host just reports the command as unknown, and it’s up to the person trying to get the thing running to do something about it, which would typically mean creating a module that registers the required command, and either implements it in full or translates it to another available command. This even means that it’s perfectly possible to create and even distribute a module that relies on functionality that has never been implemented. Why would anyone do that? Unclear, but it’s possible, and it could happen in case someone makes a module meant to work with another module and that other module hasn’t been created yet, so by making it possible to use commands that haven’t yet been implemented we can untangle situations where person A must wait for person B to implement something before they can implement something that relies on it, the two persons would at most only need to agree on the command that would be used between their modules and then work independently.

So the essential conceptual difference is that whereas operating systems and libraries define what functionality is available to programs and programs must use available functionality, here WAHE modules do the reverse, they define the functionality they need, and it’s up to anyone who wants to use them to make sure such functionality is available. Therefore whoever creates a WAHE module doesn’t actually need to worry about the availability of a command, for instance it would be better if they used a widely implemented command like Open file dialog, but if they’re not aware that such a command exists and choose to write የፋይል ንግግር ይክፈቱ instead this can work too, anyone with basic coding skills who tries to use such a module can make it work by registering this exotic command in a module and upon reception of this command call the more common command, then translate the message it returns. This way even a module made in a way that is completely divergent from the common way of making modules communicate can still always be made to work, because there is no true hard standard that modules must adhere to.

Therefore the problem of portability isn’t merely displaced by adding an intermediate layer between your code and the operating system by having Open file dialog be a way to replace GetOpenFileNameW(), to create a fixed intermediate layer between modules and the operating system would be almost pointless as it could still change and make everything that relies on it break, that would be basically equivalent to creating yet another operating system. Instead the problem of portability is solved forever by letting WAHE modules ask for whatever they want whether it ever existed or not, and if old modules rely on commands that have fallen out of favour it’s almost trivial for anyone to create a module that would bridge the gap between the old ways and the new ways.