nandi/frqpublic Fork 0
35994d45d04d2f4d7320773e58286de34ac4085a
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

frq_core.nim · 158 lines · 6.3 KBNim Blame HistoryRaw
One frontend where there were three, and a core that is not Clojure 438b247 nandi yesterday1## The C ABI, and nothing else.
2##
3## Every exported symbol is here so there is one file to read when asking what
4## the Dart side can call. The logic lives under `frq/` in ordinary Nim with
5## ordinary Nim types, which is what lets the tests test the rules rather than
6## the marshalling.
7##
8## Two conventions, both of which the bindings in `flutter/src/frq/core/`
9## wrap so no call site has to remember them:
10##
11## * Every returned string is the **caller's** to free, with `frq_free`. Nim's
12## allocator is not Dart's, so a `free()` on this side of the boundary is
13## undefined behaviour rather than a leak you can live with.
14## * `frq_init` runs once before anything else. Nim's runtime needs setting up
15## and `--app:lib` does not do it for you on every platform.
16##
17## Answers that are not a single string come back as JSON. A struct would mean
18## both sides agreeing on a memory layout, and a field added later would be a
19## version skew that segfaults rather than one that fails; JSON costs a parse
20## per call, which is nothing against the network round trip that produced the
21## line being parsed.
22
23import std/json
A message in #test, from Nim 35994d4 nandi yesterday24import frq/[ircparse, ui, state, irc]
Nim owns the screen, Dart owns the pixels 43a02c2 nandi yesterday25import frq/screens/connect as connectScreen
A message in #test, from Nim 35994d4 nandi yesterday26import frq/screens/chat as chatScreen
One frontend where there were three, and a core that is not Clojure 438b247 nandi yesterday27
28proc NimMain() {.importc.}
29
30var initialised = false
31
32proc frq_init*() {.exportc, dynlib.} =
33 ## Set Nim's runtime up. Idempotent, because a binding that guesses wrong
34 ## about whether it has been called should be harmless rather than fatal.
35 if not initialised:
36 NimMain()
37 initialised = true
38
39proc dup(s: string): cstring =
40 ## A copy of `s` that outlives this call, for the caller to `frq_free`.
41 ## `allocShared0` and not `alloc0`: the Dart side may free it from a
42 ## different thread than the one that made it.
43 let n = s.len
44 let p = cast[cstring](allocShared0(n + 1))
45 if n > 0:
46 copyMem(p, unsafeAddr s[0], n)
47 p
48
49proc frq_free*(p: cstring) {.exportc, dynlib.} =
50 ## Free what one of the functions below returned. Null is fine.
51 if p != nil:
52 deallocShared(p)
53
54proc frq_version*(): cstring {.exportc, dynlib.} =
55 ## Static storage, deliberately: this one is NOT freed, and is the only
56 ## exception to the rule above. It exists so a binding can check at load
57 ## time that the library it found is the one it was built against.
58 "0.1.0"
59
60# ----------------------------------------------------------------- irc/parse
61
62proc frq_irc_parse_line*(line: cstring): cstring {.exportc, dynlib.} =
63 ## An IRC line as JSON: `{raw, tags, account, prefix, command, params}`.
64 ##
65 ## `tags`, `account` and `prefix` are JSON null where the line carried none,
66 ## which is the distinction `frq.irc.parse` draws with nil and every caller
67 ## of it depends on — a PRIVMSG from a server with no prefix is not the same
68 ## line as one from a nick.
69 if line == nil: return dup("null")
70 let p = parseLine($line)
71 var o = newJObject()
72 o["raw"] = %p.raw
73 o["tags"] = if p.hasTags: %p.tags else: newJNull()
74 o["account"] = if p.hasAccount: %p.account else: newJNull()
75 o["prefix"] = if p.hasPrefix: %p.prefix else: newJNull()
76 o["command"] = %p.command
77 o["params"] = %p.params
78 dup($o)
79
80proc frq_irc_tag_value*(tags, key: cstring): cstring {.exportc, dynlib.} =
81 ## One tag's value, unescaped — or **null** where the tag is absent or
82 ## empty, which IRCv3 says are the same thing. Null and not "" on purpose:
83 ## see `tagValue`.
84 if tags == nil or key == nil: return nil
85 let (v, ok) = tagValue($tags, $key)
86 if ok: dup(v) else: nil
87
88proc frq_irc_unescape_tag*(v: cstring): cstring {.exportc, dynlib.} =
89 if v == nil: return nil
90 dup(unescapeTag($v))
91
92proc frq_irc_escape_tag_value*(v: cstring): cstring {.exportc, dynlib.} =
93 if v == nil: return dup("")
94 dup(escapeTagValue($v))
95
96proc frq_irc_nick_of*(prefix: cstring): cstring {.exportc, dynlib.} =
97 if prefix == nil: return nil
98 dup(nickOf($prefix))
Nim owns the screen, Dart owns the pixels 43a02c2 nandi yesterday99
100# ------------------------------------------------------------------- the UI
101#
102# The spike's real claim: Nim owns the state and the screen, Dart owns the
103# pixels, and the only things crossing are a tree going out and an event id
104# coming back. See `frq/ui.nim`.
105
A message in #test, from Nim 35994d4 nandi yesterday106proc currentTree(): string =
107 ## Whichever screen the state says. `drain` first, so the tree Dart gets is
108 ## built after every line that had arrived when it asked — that is the whole
109 ## of the polling model, and it is why there is no callback into Dart.
110 drain()
111 case app.screen
112 of scChat: $chatScreen.chatScreen(app).toJson
113 else: $connectScreen.connectScreen(app).toJson
114
Nim owns the screen, Dart owns the pixels 43a02c2 nandi yesterday115proc frq_ui_render*(): cstring {.exportc, dynlib.} =
A message in #test, from Nim 35994d4 nandi yesterday116 ## The current screen as a widget tree, in JSON.
117 ##
118 ## No longer pure, and the change is worth naming: it drains the socket's
119 ## queue first, so two calls with no dispatch between can differ when a line
120 ## arrived in the gap. That is the point — it is how the room fills — but it
121 ## means the renderer must be free to call this whenever it likes, which is
122 ## what the Dart side's poll timer does.
123 dup(currentTree())
Nim owns the screen, Dart owns the pixels 43a02c2 nandi yesterday124
125proc frq_ui_dispatch*(event: cstring): cstring {.exportc, dynlib.} =
126 ## Apply an event and answer with the tree it produced.
127 ##
128 ## One call rather than dispatch-then-render, and not to save a crossing:
129 ## it makes the pair atomic. Two calls leave a window in which Dart could
130 ## render a state nothing asked for, which is the sort of thing that shows
131 ## up once a week and never in a test.
132 ##
133 ## A malformed event is ignored rather than fatal — it arrives from a tree
134 ## the renderer may have been holding for a frame, which is a normal race.
135 if event != nil:
136 try:
137 dispatch(parseJson($event))
138 except JsonParsingError:
139 discard
A message in #test, from Nim 35994d4 nandi yesterday140 dup(currentTree())
141
142proc frq_ui_poll*(): cstring {.exportc, dynlib.} =
143 ## The tree, for a renderer that is asking because time passed rather than
144 ## because anything happened. Identical to `frq_ui_render` — named
145 ## separately so the Dart side reads as what it means.
146 dup(currentTree())
147
148proc frq_ui_offline*() {.exportc, dynlib.} =
149 ## Stop `connect` from opening a socket, for a test that wants the screens
150 ## without the network. There is no way back — a process that has asked for
151 ## this is a test process.
152 goOffline()
Nim owns the screen, Dart owns the pixels 43a02c2 nandi yesterday153
154proc frq_ui_reset*() {.exportc, dynlib.} =
155 ## Back to a fresh state. For tests, and for a renderer that wants a known
156 ## starting point rather than whatever the last run left.
A message in #test, from Nim 35994d4 nandi yesterday157 irc.stop()
Nim owns the screen, Dart owns the pixels 43a02c2 nandi yesterday158 app = initState()