|
| 1 | +# Contract Design |
| 2 | + |
| 3 | +This document outlines the ideas for the version 1.0.0 breaking changes to both the client to mGBA-http calls and the mGBA-http to mGBA Lua script calls. |
| 4 | + |
| 5 | +The improvements need to adhere to the [Design](Design.md) document around how easy it is to use for a novice or even a non-programmer as well as being as close to mGBA as possible. |
| 6 | + |
| 7 | +## HTTP API design changes |
| 8 | + |
| 9 | +### Current |
| 10 | + |
| 11 | +Client to mGBA-http relies on HTTP. Pre-1.0.0 it has the downsides of: |
| 12 | + |
| 13 | +- Inconsistent types |
| 14 | + - The `writex` endpoints use a mix of `int` and `uint` |
| 15 | + - Addresses are as strings |
| 16 | +- There are two `boolean` representations as `true` and `1` |
| 17 | +- Return types are all `text/plain` |
| 18 | +- `memoryDomain` and `regName` should be enums but are strings |
| 19 | +- Commas are used for hex byte pairs (`d3,00,00,ea`), memory domain names (`cart2,wram,cart0`), and button names (`A,B,Start`) and the client has to transform them |
| 20 | + |
| 21 | +### Improvement ideas |
| 22 | + |
| 23 | +1. Unify the datatypes propertly `int`/`uint` etc |
| 24 | +1. Have the HTTP side take in and return JSON objects as that's just much more normal for APIs compared to `text/plain` which then needs extra processing |
| 25 | +1. Proper enums |
| 26 | +1. Use a single boolean representation |
| 27 | + |
| 28 | +#### Text vs JSON |
| 29 | + |
| 30 | +The big choice is whether to move from `text/plain`, extremely simple and matches the mGBA design (both design constraints) to `application/json`. The rest are necessary fixes. It does give a bit more overhead, but any modern language with JSON capabilities is so easily equipped to do this that it's a non-issue. It might even *be* an issue to have to deal with raw strings instead of assumed API JSON objects. |
| 31 | + |
| 32 | +## Socket wire changes |
| 33 | + |
| 34 | +### Current |
| 35 | + |
| 36 | +The pre-1.0.0 socket work has the downsides of: |
| 37 | + |
| 38 | +- Each message is a four field string of `$"{Type},{Value1},{Value2},{Value3}"` even if not all the fields are used |
| 39 | +- The separator is a comma, which is also valid input for some calls |
| 40 | +- Inconsistent list separators: |
| 41 | + - `;`: button lists |
| 42 | + - `,`: hex bytes and domain names |
| 43 | +- Weird bracket data type for just `loadStateBuffer`, `[aa,bb,cc]` |
| 44 | +- Byte calls have extra overhead by having commas separating byte pairs |
| 45 | +- There is both `<|SUCCESS|>` and `<|ERROR|>` |
| 46 | +- mGBA-http doesn't confirm whether the script is the right version for the wire shape |
| 47 | + |
| 48 | +### Improvement ideas |
| 49 | + |
| 50 | +1. A single separator for lists: `,` |
| 51 | +1. A field separator that isn't a comma: `|` or `\n` |
| 52 | +1. Stop doing the comma separated, square bracket format for bytes (`[aa,bb,cc]`). Just do raw bytes `aabbcc` |
| 53 | +1. Have the status as part of the message `OK|value` and `ERR|message` |
| 54 | +1. Escape `,` and `|` with their HTML percent codes |
| 55 | +1. Simply end terminator to just `<END>` |
| 56 | +1. Remove both `<|SUCCESS|>` and `<|ERROR|>` |
| 57 | +1. Send only as many parameters as the command needs |
| 58 | +1. Length-prefix framing instead of a terminator |
| 59 | + |
| 60 | +#### Terminator vs length-prefix framing |
| 61 | + |
| 62 | +Length-prefix framing makes it easier for the consumer to deal with data as the length is up front as they know to loop until the n bytes are read. But it requires the sender to know the size of the message then prepend the length. Programatically it's barely harder than appending a terminator. However, I've opted to continue with a terminator for the sole reason of: It's easier to hand write and eyeball a request with a terminator. |
| 63 | + |
| 64 | +For example, getting the game title in Bash: |
| 65 | + |
| 66 | +```bash |
| 67 | +# terminator |
| 68 | +printf 'core.getGameTitle<END>' | nc -w1 localhost 8888 |
| 69 | + |
| 70 | +# length-prefix |
| 71 | +msg='core.getGameTitle' |
| 72 | +len=$(printf '%s' "$msg" | wc -c) |
| 73 | +printf '%s:%s' "$len" "$msg" | nc -w1 localhost 8888 |
| 74 | +``` |
| 75 | + |
| 76 | +Or Python: |
| 77 | + |
| 78 | +```python |
| 79 | +# terminator |
| 80 | +def send_terminated(sock, message): |
| 81 | + sock.sendall((message + "<END>").encode()) |
| 82 | + received = b"" |
| 83 | + while b"<END>" not in received: |
| 84 | + chunk = sock.recv(8192) |
| 85 | + if not chunk: |
| 86 | + raise ConnectionError("socket closed") |
| 87 | + received += chunk |
| 88 | + return received.split(b"<END>", 1)[0].decode() |
| 89 | + |
| 90 | +MAX_FRAME = 8 * 1024 * 1024 |
| 91 | + |
| 92 | +# length-prefix |
| 93 | +def send_length_prefixed(sock, message): |
| 94 | + body = message.encode() |
| 95 | + sock.sendall(f"{len(body)}:".encode() + body) |
| 96 | + |
| 97 | + digits = b"" |
| 98 | + while True: |
| 99 | + ch = sock.recv(1) |
| 100 | + if not ch: |
| 101 | + raise ConnectionError("socket closed") |
| 102 | + if ch == b":": |
| 103 | + break |
| 104 | + digits += ch |
| 105 | + |
| 106 | + expected = int(digits) |
| 107 | + if expected > MAX_FRAME: |
| 108 | + raise ValueError(f"frame too large: {expected}") |
| 109 | + |
| 110 | + payload = b"" |
| 111 | + while len(payload) < expected: |
| 112 | + chunk = sock.recv(expected - len(payload)) |
| 113 | + if not chunk: |
| 114 | + raise ConnectionError("socket closed") |
| 115 | + payload += chunk |
| 116 | + |
| 117 | + return payload.decode() |
| 118 | +``` |
| 119 | + |
| 120 | +A hard to see problem is that it needs to count bytes and not character length. Running `console.log|café` will end up causing junk in further messages as it's 16 characters, but 17 bytes. |
| 121 | + |
| 122 | +However, if it wasn't for the easier reading and hand writing for a terminator, fixed-length would be best for this case. |
0 commit comments