added: bumped 'command' library for --version support
feature (0.6.0): built out a robust virutal device system and refactored the project components
changed (0.5.0): device buffers are now 8B, dev opcodes merge index/flag and drop offset, stash opcode is top of stacks instead of bitmask
A device-extensible execution environment for the Coalescent Computer.
At its core, Cohost is an implementation of the Coalescent Core virtual CPU. The Coalescent Core is a stack machine with a Data Stack, a Return Stack, 64KB of Program Memory, 4GB of RAM Address Space, 16 Device I/O ports, and 4 Direct Memory Access Controllers. Cohost can directly execute ROM files assembled using the Co
programming language's assembler. The Coalescent Core CPU was inspired by the work done on Uxn and the book Stack Computers: The New Wave
.
Beyond just executing instructions, Cohost also provides features that allow someone to define a more complete and usable virtual system through its extensible device system. By default, Cohost will boot with a single virtual device manager plugged into port zero, which then listens for other device connections over a Unix socket. Combined with a tiny wire protocol, anyone can write virtual devices in any programming language that can output to a Unix socket, and then write device handlers directly in assembly. It currently ships with an example "Console" device that can be launched and attached via the cohost
CLI.
To build from source, you must have the Rust toolchain installed on your machine. Given that, you can install with:
git clone https://git.sr.ht/~jakintosh/cohost
cargo install --path cohost
You can now use the cohost
CLI via the command line. The program binary as of 0.6.0 using Rust 1.71.0 is 498kb on Arch Linux.
The cohost
CLI has two subcommand branches: run
for executing binaries, and device
for launching and attaching devices.
cohost run
To run the machine, first you need to build a ROM using the co toolchain ( https://git.sr.ht/~jakintosh/co ), or with any other process that produces a valid COINS binary. From there, you can point to that binary file using the command's only required parameter:
cohost run <rom-file>
The run
command has three options:
-d/--debug
: shows a step-by-step debug execution visualizer (press ENTER to step)-p/--profile
: shows a profile of the program on completion (program must gracefully end)-t/--terminates
: the CPU will exit when it returns with an empty return stack instead of becoming idle, which is the default behaviorRealistically, --profile
can only be used right now in conjunction with --terminates
, since otherwise the program never gracefully exits to show profiler information. Profile currently only shows the total number of executed instructions.
Two equivalent real-world examples using options:
cohost run -dt stack-test.rom
cohost run --debug --terminates stack-test.rom
cohost device
The cohost
CLI also includes the ability to launch and attach a console to the running instance.
cohost device console
The console allows one to send text data into the running cohost instance, and also display text that is output from the machine.
To write your own device, you need to connect to a Unix socket at /tmp/cohost/devices.sock
, and then send a handshake message in plain text with the format "Device-Type: {type}\n"
or "Device-Id: {id}\n"
. The supported type
for the "Device-Type"
format is currently only "Console"
. The "Device-Id"
handshake is currently not supported, but will eventually expect a base64 encoding of a 256-bit Co routine hash. For all intents and purposes, at the time of this writing the only valid handshake message is "Device-Type: Console\n"
.
Once a handshake is sent, the remaining data over the wire is expected in a binary format. It is read 9 bytes at a time, where the first byte is the single byte status register of a Coalescent Core device slot, and the 1st-9th bytes are the 8 bytes of the device slot's data buffer.
Wire Message
[ Status ] [ buffer ]
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
The status register itself uses the two highest bits as flags, and the bottom three bits as length indicators.
The highest bit is the SEND flag, which is used to signal that the data in the buffer is ready to be sent. When implementing a virtual device, this bit should always be set, because the virtual devices only communicate when sending a buffer.
The second highest bit is the LOCK flag, which signals that the buffer has been locked by a writer. This bit should NOT be set if the current message being sent is complete. In practice, sending SEND & LOCK flag means that the message has more data on the way, and SEND flag alone means this is the end of the buffer.
Finally, the bottom three bits represent the index of the highest byte of valid data in the buffer. If the entire 8 bit buffer is valid data, the highest index would be 7, and so the bottom three bits should be 111
. You cannot send an empty buffer, so the smallest value would be 000
, signaling that the byte in the 0th index is the highest valid byte, meaning one byte.
Status Register
SEND LOCK NULL NULL NULL [ INDEX ]
0b0 0b0 0b0 0b0 0b0 0b0 0b0 0b0
As an example, to send the message "Hello!" over the wire (assuming unicode/ASCII) using this wire protocol would look like this:
0b10000110 0x48 0x65 0x6C 0x6C 0x6F 0x21 0x0A 0x00
Furthermore, to send the message Hello, world!
would look like this:
0b11000111 0x48 0x65 0x6C 0x6C 0x6F 0x2C 0x20 0x77
0b10000101 0x6F 0x72 0x6C 0x64 0x21 0x0A 0x00 0x00