Skip to content

Commit ec13e59

Browse files
committed
Create ContractDesign.md
1 parent 8f165f3 commit ec13e59

1 file changed

Lines changed: 122 additions & 0 deletions

File tree

‎docs/ContractDesign.md‎

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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

Comments
 (0)