nandi/frqpublic Fork 0
56551a8157331acb8601841b63f2dc7424339722
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 · 198 lines · 7.8 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 23h ago24import frq/[ircparse, ui, state, irc]
Nim under the existing UI, not instead of it 56551a8 nandi 22h ago25import frq/conn as tr
26import frq/trace
Nim owns the screen, Dart owns the pixels 43a02c2 nandi 23h ago27import frq/screens/connect as connectScreen
A message in #test, from Nim 35994d4 nandi 23h ago28import frq/screens/chat as chatScreen
One frontend where there were three, and a core that is not Clojure 438b247 nandi yesterday29
30proc NimMain() {.importc.}
31
32var initialised = false
33
34proc frq_init*() {.exportc, dynlib.} =
35 ## Set Nim's runtime up. Idempotent, because a binding that guesses wrong
36 ## about whether it has been called should be harmless rather than fatal.
37 if not initialised:
38 NimMain()
39 initialised = true
40
41proc dup(s: string): cstring =
42 ## A copy of `s` that outlives this call, for the caller to `frq_free`.
43 ## `allocShared0` and not `alloc0`: the Dart side may free it from a
44 ## different thread than the one that made it.
45 let n = s.len
46 let p = cast[cstring](allocShared0(n + 1))
47 if n > 0:
48 copyMem(p, unsafeAddr s[0], n)
49 p
50
51proc frq_free*(p: cstring) {.exportc, dynlib.} =
52 ## Free what one of the functions below returned. Null is fine.
53 if p != nil:
54 deallocShared(p)
55
56proc frq_version*(): cstring {.exportc, dynlib.} =
57 ## Static storage, deliberately: this one is NOT freed, and is the only
58 ## exception to the rule above. It exists so a binding can check at load
59 ## time that the library it found is the one it was built against.
60 "0.1.0"
61
62# ----------------------------------------------------------------- irc/parse
63
64proc frq_irc_parse_line*(line: cstring): cstring {.exportc, dynlib.} =
65 ## An IRC line as JSON: `{raw, tags, account, prefix, command, params}`.
66 ##
67 ## `tags`, `account` and `prefix` are JSON null where the line carried none,
68 ## which is the distinction `frq.irc.parse` draws with nil and every caller
69 ## of it depends on — a PRIVMSG from a server with no prefix is not the same
70 ## line as one from a nick.
71 if line == nil: return dup("null")
72 let p = parseLine($line)
73 var o = newJObject()
74 o["raw"] = %p.raw
75 o["tags"] = if p.hasTags: %p.tags else: newJNull()
76 o["account"] = if p.hasAccount: %p.account else: newJNull()
77 o["prefix"] = if p.hasPrefix: %p.prefix else: newJNull()
78 o["command"] = %p.command
79 o["params"] = %p.params
80 dup($o)
81
82proc frq_irc_tag_value*(tags, key: cstring): cstring {.exportc, dynlib.} =
83 ## One tag's value, unescaped — or **null** where the tag is absent or
84 ## empty, which IRCv3 says are the same thing. Null and not "" on purpose:
85 ## see `tagValue`.
86 if tags == nil or key == nil: return nil
87 let (v, ok) = tagValue($tags, $key)
88 if ok: dup(v) else: nil
89
90proc frq_irc_unescape_tag*(v: cstring): cstring {.exportc, dynlib.} =
91 if v == nil: return nil
92 dup(unescapeTag($v))
93
94proc frq_irc_escape_tag_value*(v: cstring): cstring {.exportc, dynlib.} =
95 if v == nil: return dup("")
96 dup(escapeTagValue($v))
97
98proc frq_irc_nick_of*(prefix: cstring): cstring {.exportc, dynlib.} =
99 if prefix == nil: return nil
100 dup(nickOf($prefix))
Nim owns the screen, Dart owns the pixels 43a02c2 nandi 23h ago101
102# ------------------------------------------------------------------- the UI
103#
104# The spike's real claim: Nim owns the state and the screen, Dart owns the
105# pixels, and the only things crossing are a tree going out and an event id
106# coming back. See `frq/ui.nim`.
107
A message in #test, from Nim 35994d4 nandi 23h ago108proc currentTree(): string =
The window connects 1d62d1a nandi 23h ago109 maybeAutoconnect()
A message in #test, from Nim 35994d4 nandi 23h ago110 ## Whichever screen the state says. `drain` first, so the tree Dart gets is
111 ## built after every line that had arrived when it asked — that is the whole
112 ## of the polling model, and it is why there is no callback into Dart.
113 drain()
114 case app.screen
115 of scChat: $chatScreen.chatScreen(app).toJson
116 else: $connectScreen.connectScreen(app).toJson
117
Nim owns the screen, Dart owns the pixels 43a02c2 nandi 23h ago118proc frq_ui_render*(): cstring {.exportc, dynlib.} =
A message in #test, from Nim 35994d4 nandi 23h ago119 ## The current screen as a widget tree, in JSON.
120 ##
121 ## No longer pure, and the change is worth naming: it drains the socket's
122 ## queue first, so two calls with no dispatch between can differ when a line
123 ## arrived in the gap. That is the point — it is how the room fills — but it
124 ## means the renderer must be free to call this whenever it likes, which is
125 ## what the Dart side's poll timer does.
126 dup(currentTree())
Nim owns the screen, Dart owns the pixels 43a02c2 nandi 23h ago127
128proc frq_ui_dispatch*(event: cstring): cstring {.exportc, dynlib.} =
129 ## Apply an event and answer with the tree it produced.
130 ##
131 ## One call rather than dispatch-then-render, and not to save a crossing:
132 ## it makes the pair atomic. Two calls leave a window in which Dart could
133 ## render a state nothing asked for, which is the sort of thing that shows
134 ## up once a week and never in a test.
135 ##
136 ## A malformed event is ignored rather than fatal — it arrives from a tree
137 ## the renderer may have been holding for a frame, which is a normal race.
138 if event != nil:
139 try:
140 dispatch(parseJson($event))
141 except JsonParsingError:
142 discard
A message in #test, from Nim 35994d4 nandi 23h ago143 dup(currentTree())
144
145proc frq_ui_poll*(): cstring {.exportc, dynlib.} =
146 ## The tree, for a renderer that is asking because time passed rather than
147 ## because anything happened. Identical to `frq_ui_render` — named
148 ## separately so the Dart side reads as what it means.
149 dup(currentTree())
150
151proc frq_ui_offline*() {.exportc, dynlib.} =
152 ## Stop `connect` from opening a socket, for a test that wants the screens
153 ## without the network. There is no way back — a process that has asked for
154 ## this is a test process.
155 goOffline()
Nim owns the screen, Dart owns the pixels 43a02c2 nandi 23h ago156
157proc frq_ui_reset*() {.exportc, dynlib.} =
158 ## Back to a fresh state. For tests, and for a renderer that wants a known
159 ## starting point rather than whatever the last run left.
A message in #test, from Nim 35994d4 nandi 23h ago160 irc.stop()
Nim owns the screen, Dart owns the pixels 43a02c2 nandi 23h ago161 app = initState()
Nim under the existing UI, not instead of it 56551a8 nandi 22h ago162
163
164# --------------------------------------------------------------- transport
165#
166# `frq.net`'s three operations, for `frq.net.nim` to install. This is the
167# wiring that matters: the existing ClojureDart screens, cells and actions are
168# untouched, and only the socket underneath them becomes Nim.
169#
170# Polled rather than callback-driven, for the reason the UI is: a Dart callback
171# invoked from a foreign thread has to be marshalled onto the main isolate, and
172# a timer on the Dart side does the same job with no mechanism at all.
173
174proc frq_trace*(topic, msg: cstring) {.exportc, dynlib.} =
175 ## Let the Dart side log through the same facility, so one FRQ_TRACE=1 gives
176 ## one interleaved story instead of two half-ones in different places.
177 if topic != nil and msg != nil:
178 trace($topic, $msg)
179
180proc frq_conn_open*(host: cstring, port: cint, tls: cint) {.exportc, dynlib.} =
181 if host == nil: return
182 tr.open(tr.ConnConfig(host: $host, port: port.int, tls: tls != 0))
183
184proc frq_conn_send*(line: cstring) {.exportc, dynlib.} =
185 if line != nil: tr.send($line)
186
187proc frq_conn_close*() {.exportc, dynlib.} =
188 tr.close()
189
190proc frq_conn_recv*(): cstring {.exportc, dynlib.} =
191 ## The next line, or null when there is none waiting. Never blocks.
192 let (ok, line) = tr.tryLine()
193 if ok: dup(line) else: nil
194
195proc frq_conn_event*(): cstring {.exportc, dynlib.} =
196 ## The next transport event — "open", "close: …", "error: …" — or null.
197 let (ok, e) = tr.tryEvent()
198 if ok: dup(e) else: nil