logo

drewdevault.com

[mirror] blog and personal website of Drew DeVault git clone https://hacktivis.me/git/mirror/drewdevault.com.git

Porting-an-entire-toolchain-to-the-browser-with-emscripten.md (15538B)


  1. ---
  2. date: 2014-11-30
  3. # vim: tw=80
  4. layout: post_toolchain
  5. title: Porting an assembler, debugger, and more to WebAssembly
  6. tags: [wasm, javascript, KnightOS]
  7. ---
  8. WebAssembly is pretty cool! It lets you write portable C and cross-compile it to
  9. JavaScript so it'll run in a web browser. As the maintainer of
  10. [KnightOS](http://www.knightos.org), I looked to WASM as a potential means
  11. of reducing the cost of entry for new developers hoping to target the OS.
  12. <noscript>
  13. Note: this article uses JavaScript to run all of this stuff in your web browser.
  14. I don't use any third-party scripts, tracking, or anything else icky.
  15. </noscript>
  16. ## Rationale for WASM
  17. There are several pieces of software in the toolchain that are required to write
  18. and test software for KnightOS:
  19. * [scas](https://github.com/KnightOS/scas) - a z80 assembler
  20. * [genkfs](https://github.com/KnightOS/genkfs) - generates KFS filesystem images
  21. * [kpack](https://github.com/KnightOS/kpack) - packaging tool, like makepkg on Arch Linux
  22. * [z80e](https://github.com/KnightOS/z80e) - a z80 calculator emulator
  23. You also need a copy of the latest kernel and any of your dependencies from
  24. [packages.knightos.org](https://packages.knightos.org). Getting all of this is
  25. not straightforward. On Linux and Mac, there are no official packages for any of
  26. these tools. On Windows, there are still no official packages, and you have to
  27. use Cygwin on top of that. The first step to writing KnightOS programs is to
  28. manually compile and install several tools, which is a lot to ask of someone who
  29. just wants to experiment.
  30. All of the tools in our toolchain are written in C. We saw WASM as an
  31. opportunity to reduce all of this effort into simply firing up your web browser.
  32. It works, too! Here's what was involved.
  33. >**Note**: Click the screen on the emulator to the left to give it your
  34. >keyboard. Click away to take it back. You can use your arrow keys, F1-F5,
  35. >enter, and escape (as MODE).
  36. ## The final product
  37. Let's start by showing you what we've accomplished. It's now possible for
  38. curious developers to try out KnightOS programming in their web browser. Of
  39. course, they still have to do it in assembly, but we're [working on
  40. that](https://github.com/KnightOS/kcc) 😉. Here's a "hello world" you can run in
  41. your web browser:
  42. <div class="demo">
  43. <div class="editor"
  44. data-source="/sources/helloworld.asm"
  45. data-file="main.asm"></div>
  46. <div class="calculator-wrapper">
  47. <div class="calculator">
  48. <canvas width="385" height="256" class="emulator-screen"></canvas>
  49. </div>
  50. </div>
  51. </div>
  52. We can also install new dependencies on the fly and use them in our programs.
  53. Here's another program that draws the "hello world" message in a window. You
  54. should install `core/corelib` first:
  55. <input type="text" id="package-name" value="core/corelib" />
  56. <input type="button" id="install-package" value="Install" />
  57. <div class="demo">
  58. <div class="editor" data-source="/sources/corelib-hello.asm" data-file="main.asm"></div>
  59. <div class="calculator-wrapper">
  60. <div class="calculator">
  61. <canvas width="385" height="256" class="emulator-screen"></canvas>
  62. </div>
  63. </div>
  64. </div>
  65. You can find more packages to try out on
  66. [packages.knightos.org](https://packages.knightos.org). Here's another example,
  67. this one launches the file manager. You'll have to install a few packages for it
  68. to work:
  69. Install:
  70. <input type="button" class="install-package-button" data-package="extra/fileman" value="extra/fileman" />
  71. <input type="button" class="install-package-button" data-package="core/configlib" value="core/configlib" />
  72. <input type="button" class="install-package-button" data-package="core/corelib" value="core/corelib" />
  73. <div class="demo">
  74. <div class="editor" data-source="/sources/fileman.asm" data-file="main.asm"></div>
  75. <div class="calculator-wrapper">
  76. <div class="calculator">
  77. <canvas width="385" height="256" class="emulator-screen"></canvas>
  78. </div>
  79. </div>
  80. </div>
  81. Feel free to edit any of these examples! You can run them again with the Run
  82. button. These resources might be useful if you want to play with this some more:
  83. [z80 instruction set](http://www.z80.info/z80-op.txt) - [z80 assembly tutorial](http://tutorials.eeems.ca/ASMin28Days/lesson/toc.html) - [KnightOS reference documentation](http://www.knightos.org/documentation/reference)
  84. Note: our toolchain has some memory leaks, so eventually WASM is going to
  85. run out of memory and then you'll have to refresh. Sorry!
  86. ## How all of the pieces fit together
  87. When you
  88. loaded this page, a bunch of things happened. First, the [latest
  89. release](https://github.com/KnightOS/kernel/releases) of the [KnightOS
  90. kernel](https://github.com/KnightOS/kernel) was downloaded. Then all of the
  91. WASM ports of the toolchain were downloaded and loaded. Some virtual filesystems
  92. were set up, and two KnightOS packages were downloaded and installed:
  93. [`core/init`](https://packages.knightos.org/core/init), and
  94. [`core/kernel-headers`](https://packages.knightos.org/core/kernel-headers),
  95. respectively necessary for booting the system and compiling code against the
  96. kernel API. Extracting those packages involves copying them into kpack's
  97. virtual filesystem and running `kpack -e path/to/package root/`.
  98. When you click "Run" on one of these text boxes, the contents of the text box is
  99. written to `/main.asm` in the assembler's virtual filesystem. The package
  100. installation process extracts headers to `/include/`, and scas itself is run
  101. with `/main.asm -I/include -o /executable`, which assembles the program and
  102. writes the output to `/executable`.
  103. Then we copy the executable into the genkfs filesystem (this is the tool that
  104. generates filesystem images). We also copy the empty kernel into this
  105. filesystem, as well as any of the packages we've installed. We then run `genkfs
  106. /kernel.rom /root`, which creates a filesystem image from `/root` and bakes it
  107. into `kernel.rom`. This produces a ready-to-emulate ROM image that we can load
  108. into the z80e emulator on the left.
  109. ## The WASM details
  110. Porting all this stuff to WASM wasn't straightforward. The easiest part
  111. was cross-compiling all of them to JavaScript:
  112. cd build
  113. emconfigure cmake ..
  114. emmake make
  115. The process was basically that simple for each piece of software. There were
  116. [a](https://github.com/KnightOS/genkfs/commit/c4eefa87a3b5bdbafcc6d971654608c594f779a1)
  117. [few](https://github.com/KnightOS/scas/commit/d2044e7d7586a946422ce6493cc6dff01127d1c2)
  118. [changes](https://github.com/KnightOS/scas/commit/8bc31af28e8419a9fa6c421147ea522935bd0df4)
  119. made to some of the tools to fix a few problems. The hard part
  120. came when I wanted to run all of them on the same page. WASM compiled code
  121. assumes that it will be the only WASM module on the page at any given
  122. time, so this was a bit challenging and involved editing the generated JS.
  123. The first thing I did was wrap all of the modules in isolated AMD loaders[^1].
  124. You can see how some of this ended up looking by visiting the actual scripts
  125. (warning, big files):
  126. [^1]: AMD was an early means of using modules with JavaScript, which was popular at the time this article was written (2014). Today, a different form of modules has become part of the JavaScript language standard.
  127. * [scas.js](/tools/scas.js)
  128. * [kpack.js](/tools/kpack.js)
  129. * [genkfs.js](/tools/genkfs.js)
  130. That was enough to make it so that they could all run. These are part of a
  131. toolchain, though, so somehow they needed to share files. Emscripten's [FS
  132. object](http://kripken.github.io/emscripten-site/docs/api_reference/Filesystem-API.html)
  133. cannot be shared between modules, so the solution was to write a little JS:
  134. ```coffeescript
  135. copy_between_systems = (fs1, fs2, from, to, encoding) ->
  136. for f in fs1.readdir(from)
  137. continue if f in ['.', '..']
  138. fs1p = from + '/' + f
  139. fs2p = to + '/' + f
  140. s = fs1.stat(fs1p)
  141. log("Writing #{fs1p} to #{fs2p}")
  142. if fs1.isDir(s.mode)
  143. try
  144. fs2.mkdir(fs2p)
  145. catch
  146. # pass
  147. copy_between_systems(fs1, fs2, fs1p, fs2p, encoding)
  148. else
  149. fs2.writeFile(fs2p, fs1.readFile(fs1p, { encoding: encoding }), { encoding: encoding })
  150. ```
  151. With this, we can extract packages in the kpack filesystem and copy them to the
  152. genkfs filesystem:
  153. ```coffeescript
  154. install_package = (repo, name, callback) ->
  155. full_name = repo + '/' + name
  156. log("Downloading " + full_name)
  157. xhr = new XMLHttpRequest()
  158. xhr.open('GET', "https://packages.knightos.org/" + full_name + "/download")
  159. xhr.responseType = 'arraybuffer'
  160. xhr.onload = () ->
  161. log("Installing " + full_name)
  162. file_name = '/packages/' + repo + '-' + name + '.pkg'
  163. data = new Uint8Array(xhr.response)
  164. toolchain.kpack.FS.writeFile(file_name, data, { encoding: 'binary' })
  165. toolchain.kpack.Module.callMain(['-e', file_name, '/pkgroot'])
  166. copy_between_systems(toolchain.kpack.FS, toolchain.scas.FS, "/pkgroot/include", "/include", "utf8")
  167. copy_between_systems(toolchain.kpack.FS, toolchain.genkfs.FS, "/pkgroot", "/root", "binary")
  168. log("Package installed.")
  169. callback() if callback?
  170. xhr.send()
  171. ```
  172. And this puts all the pieces in place for us to actually pass an assembly file
  173. through our toolchain:
  174. ```coffeescript
  175. run_project = (main) ->
  176. # Assemble
  177. window.toolchain.scas.FS.writeFile('/main.asm', main)
  178. log("Calling assembler...")
  179. ret = window.toolchain.scas.Module.callMain(['/main.asm', '-I/include/', '-o', 'executable'])
  180. return ret if ret != 0
  181. log("Assembly done!")
  182. # Build filesystem
  183. executable = window.toolchain.scas.FS.readFile("/executable", { encoding: 'binary' })
  184. window.toolchain.genkfs.FS.writeFile("/root/bin/executable", executable, { encoding: 'binary' })
  185. window.toolchain.genkfs.FS.writeFile("/root/etc/inittab", "/bin/executable")
  186. window.toolchain.genkfs.FS.writeFile("/kernel.rom", new Uint8Array(toolchain.kernel_rom), { encoding: 'binary' })
  187. window.toolchain.genkfs.Module.callMain(["/kernel.rom", "/root"])
  188. rom = window.toolchain.genkfs.FS.readFile("/kernel.rom", { encoding: 'binary' })
  189. log("Loading your program into the emulator!")
  190. if current_emulator != null
  191. current_emulator.cleanup()
  192. current_emulator = new toolchain.ide_emu(document.getElementById('screen'))
  193. current_emulator.load_rom(rom.buffer)
  194. return 0
  195. ```
  196. This was fairly easy to put together once we got all the tools to cooperate.
  197. After all, these are all command-line tools. Invoking them is as simple as
  198. calling `main` and then fiddling with the files that come out. Porting z80e, on
  199. the other hand, was not nearly as simple.
  200. ## Porting z80e to the browser
  201. [z80e](https://github.com/KnightOS/z80e) is our calculator emulator. It's also
  202. written in C, but needs to interact much more closely with the user. We need to
  203. be able to render the display to a canvas, and to receive input from the user.
  204. This isn't nearly as simple as just calling `main` and playing with some files.
  205. To accomplish this, we've put together
  206. [OpenTI](https://github.com/KnightOS/OpenTI), a set of JavaScript bindings to
  207. z80e. This is mostly the work of my friend puckipedia, but I can explain a bit
  208. of what is involved. The short of it is that we needed to map native structs to
  209. JavaScript objects and pass JavaScript code as function pointers to z80e's
  210. hooks. So far as I know, the KnightOS team is the only group to have attempted
  211. something with this deep of integration between WASM and JavaScript - because we
  212. had to do a ton of the work ourselves.
  213. OpenTI contains a
  214. [wrap](https://github.com/KnightOS/OpenTI/blob/master/webui/js/OpenTI/wrap.js)
  215. module that is capable of wrapping structs and pointers in JavaScript objects.
  216. This is a tedious procedure, because we have to know the offset and size of each
  217. field in native code. An example of a wrapped object is given here:
  218. ```javascript
  219. define(["../wrap"], function(Wrap) {
  220. var Registers = function(pointer) {
  221. if (!pointer) {
  222. throw "This object can only be instantiated with a memory region predefined!";
  223. }
  224. this.pointer = pointer;
  225. Wrap.UInt16(this, "AF", pointer);
  226. Wrap.UInt8(this, "F", pointer);
  227. Wrap.UInt8(this, "A", pointer + 1);
  228. this.flags = {};
  229. Wrap.UInt8(this.flags, "C", pointer, 128, 7);
  230. Wrap.UInt8(this.flags, "N", pointer, 64, 6);
  231. Wrap.UInt8(this.flags, "PV", pointer, 32, 5);
  232. Wrap.UInt8(this.flags, "3", pointer, 16, 4);
  233. Wrap.UInt8(this.flags, "H", pointer, 8, 3);
  234. Wrap.UInt8(this.flags, "5", pointer, 4, 2);
  235. Wrap.UInt8(this.flags, "Z", pointer, 2, 1);
  236. Wrap.UInt8(this.flags, "S", pointer, 1, 0);
  237. pointer += 2;
  238. Wrap.UInt16(this, "BC", pointer);
  239. Wrap.UInt8(this, "C", pointer);
  240. Wrap.UInt8(this, "B", pointer + 1);
  241. pointer += 2;
  242. Wrap.UInt16(this, "DE", pointer);
  243. Wrap.UInt8(this, "E", pointer);
  244. Wrap.UInt8(this, "D", pointer + 1);
  245. pointer += 2;
  246. Wrap.UInt16(this, "HL", pointer);
  247. Wrap.UInt8(this, "L", pointer);
  248. Wrap.UInt8(this, "H", pointer + 1);
  249. pointer += 2;
  250. Wrap.UInt16(this, "_AF", pointer);
  251. Wrap.UInt16(this, "_BC", pointer + 2);
  252. Wrap.UInt16(this, "_DE", pointer + 4);
  253. Wrap.UInt16(this, "_HL", pointer + 6);
  254. pointer += 8;
  255. Wrap.UInt16(this, "PC", pointer);
  256. Wrap.UInt16(this, "SP", pointer + 2);
  257. pointer += 4;
  258. Wrap.UInt16(this, "IX", pointer);
  259. Wrap.UInt8(this, "IXL", pointer);
  260. Wrap.UInt8(this, "IXH", pointer + 1);
  261. pointer += 2;
  262. Wrap.UInt16(this, "IY", pointer);
  263. Wrap.UInt8(this, "IYL", pointer);
  264. Wrap.UInt8(this, "IYH", pointer + 1);
  265. pointer += 2;
  266. Wrap.UInt8(this, "I", pointer++);
  267. Wrap.UInt8(this, "R", pointer++);
  268. // 2 dummy bytes needed for 4-byte alignment
  269. }
  270. Registers.sizeOf = function() {
  271. return 26;
  272. }
  273. return Registers;
  274. });
  275. ```
  276. The result of that effort is that you can find out what the current value of a
  277. register is from some nice clean JavaScript: `asic.cpu.registers.PC` (it's <code
  278. id="register-pc"></code>, by the way). Pop open your JavaScript console and play
  279. around with the `current_asic` global!
  280. ## Conclusions
  281. I've put all of this together on [try.knightos.org](http://try.knightos.org).
  282. The source is available on
  283. [GitHub](https://github.com/KnightOS/try.knightos.org). It's entirely
  284. client-side, so it can be hosted on GitHub Pages. I'm hopeful that this will
  285. make it easier for people to get interested in KnightOS development, but it'll
  286. be a lot better once I can get more documentation and tutorials written. It'd be
  287. pretty cool if we could have interactive tutorials like this!
  288. If you, reader, are interested in working on some pretty cool shit, there's a
  289. place for you! We have things to do in Assembly, C, JavaScript, Python, and a
  290. handful of other things. Maybe you have a knack for design and want to help
  291. improve it. Whatever the case may be, if you have interest in this stuff, come
  292. hang out with us on IRC: [#knightos on
  293. irc.freenode.net](http://webchat.freenode.net/?channels=knightos&uio=d4).
  294. ---
  295. ![](https://sr.ht/zhRB.jpg)
  296. **2018-08-31**: This article was updated to fix some long-broken scripts and
  297. adjust everything to fit into the since-updated blog theme. The title was also
  298. changed from "Porting an entire desktop toolchain to the browser with
  299. Emscripten" and some minor editorial corrections were made. References to
  300. Emscripten were replaced with WebAssembly - WASM is the standard API that
  301. browsers have implemented to replace asm.js, and the Emscripten toolchain and
  302. JavaScript API remained compatible throughout the process.