nandi/frqpublic Fork 0
e505dedb6ac7169a867e5e9f7732e997d8a003f9
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 · 196 lines · 8.0 KBNim Blame HistoryRaw
One frontend where there were three, and a core that is not Clojure 438b247 nandi 18h ago1## 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.
Delete the spike that owned the screens 1bb3f77 nandi 17h ago22##
The renderer, and a transport bug that took four tries 58e6c97 nandi 15h ago23## The UI half of this ABI — `frq_ui_render` and `frq_ui_dispatch` — is Nim
24## owning the screens as well as the rules. The screens under it are ported
25## from `common/frq/screens/` rather than reimagined, which is the difference
26## between this and the experiment that was deleted for being a facsimile.
One frontend where there were three, and a core that is not Clojure 438b247 nandi 18h ago27
The renderer, and a transport bug that took four tries 58e6c97 nandi 15h ago28import std/[json, strutils]
29import frq/[ircparse, trace, ui, cells, reducer]
30import frq/conn as tr
31import frq/screens/connect as scConnectScreen
32import frq/screens/chats as scChatsScreen
33import frq/screens/chat as scChatScreen
34import frq/screens/settings as scSettingsScreen
One frontend where there were three, and a core that is not Clojure 438b247 nandi 18h ago35
36proc frq_init*() {.exportc, dynlib.} =
The renderer, and a transport bug that took four tries 58e6c97 nandi 15h ago37 ## Kept for the ABI, and deliberately empty.
38 ##
39 ## It used to call `NimMain()`. On Linux `--app:lib` already emits a library
40 ## constructor that runs Nim's module initialisers at dlopen, so calling it
41 ## again ran every module's top-level code a SECOND time — which for
42 ## `conn.nim` meant `outbound.open()` on channels that were already open,
43 ## quietly resetting them. The reader thread then drained a different queue
44 ## from the one the writer filled, and nothing this client sent ever left.
45 ##
46 ## Nothing to do here, then, but the symbol stays: a binding that calls it
47 ## should keep working, and one that does not should not have to care.
48 discard
One frontend where there were three, and a core that is not Clojure 438b247 nandi 18h ago49
50proc dup(s: string): cstring =
51 ## A copy of `s` that outlives this call, for the caller to `frq_free`.
52 ## `allocShared0` and not `alloc0`: the Dart side may free it from a
53 ## different thread than the one that made it.
54 let n = s.len
55 let p = cast[cstring](allocShared0(n + 1))
56 if n > 0:
57 copyMem(p, unsafeAddr s[0], n)
58 p
59
60proc frq_free*(p: cstring) {.exportc, dynlib.} =
61 ## Free what one of the functions below returned. Null is fine.
62 if p != nil:
63 deallocShared(p)
64
65proc frq_version*(): cstring {.exportc, dynlib.} =
66 ## Static storage, deliberately: this one is NOT freed, and is the only
67 ## exception to the rule above. It exists so a binding can check at load
68 ## time that the library it found is the one it was built against.
69 "0.1.0"
70
71# ----------------------------------------------------------------- irc/parse
72
73proc frq_irc_parse_line*(line: cstring): cstring {.exportc, dynlib.} =
74 ## An IRC line as JSON: `{raw, tags, account, prefix, command, params}`.
75 ##
76 ## `tags`, `account` and `prefix` are JSON null where the line carried none,
77 ## which is the distinction `frq.irc.parse` draws with nil and every caller
78 ## of it depends on — a PRIVMSG from a server with no prefix is not the same
79 ## line as one from a nick.
80 if line == nil: return dup("null")
81 let p = parseLine($line)
82 var o = newJObject()
83 o["raw"] = %p.raw
84 o["tags"] = if p.hasTags: %p.tags else: newJNull()
85 o["account"] = if p.hasAccount: %p.account else: newJNull()
86 o["prefix"] = if p.hasPrefix: %p.prefix else: newJNull()
87 o["command"] = %p.command
88 o["params"] = %p.params
89 dup($o)
90
91proc frq_irc_tag_value*(tags, key: cstring): cstring {.exportc, dynlib.} =
92 ## One tag's value, unescaped — or **null** where the tag is absent or
93 ## empty, which IRCv3 says are the same thing. Null and not "" on purpose:
94 ## see `tagValue`.
95 if tags == nil or key == nil: return nil
96 let (v, ok) = tagValue($tags, $key)
97 if ok: dup(v) else: nil
98
99proc frq_irc_unescape_tag*(v: cstring): cstring {.exportc, dynlib.} =
100 if v == nil: return nil
101 dup(unescapeTag($v))
102
103proc frq_irc_escape_tag_value*(v: cstring): cstring {.exportc, dynlib.} =
104 if v == nil: return dup("")
105 dup(escapeTagValue($v))
106
107proc frq_irc_nick_of*(prefix: cstring): cstring {.exportc, dynlib.} =
108 if prefix == nil: return nil
109 dup(nickOf($prefix))
Nim owns the screen, Dart owns the pixels 43a02c2 nandi 17h ago110
Nim under the existing UI, not instead of it 56551a8 nandi 17h ago111# --------------------------------------------------------------- transport
112#
113# `frq.net`'s three operations, for `frq.net.nim` to install. This is the
114# wiring that matters: the existing ClojureDart screens, cells and actions are
115# untouched, and only the socket underneath them becomes Nim.
116#
117# Polled rather than callback-driven, for the reason the UI is: a Dart callback
118# invoked from a foreign thread has to be marshalled onto the main isolate, and
119# a timer on the Dart side does the same job with no mechanism at all.
120
121proc frq_trace*(topic, msg: cstring) {.exportc, dynlib.} =
122 ## Let the Dart side log through the same facility, so one FRQ_TRACE=1 gives
123 ## one interleaved story instead of two half-ones in different places.
124 if topic != nil and msg != nil:
125 trace($topic, $msg)
126
127proc frq_conn_open*(host: cstring, port: cint, tls: cint) {.exportc, dynlib.} =
128 if host == nil: return
129 tr.open(tr.ConnConfig(host: $host, port: port.int, tls: tls != 0))
130
131proc frq_conn_send*(line: cstring) {.exportc, dynlib.} =
132 if line != nil: tr.send($line)
133
134proc frq_conn_close*() {.exportc, dynlib.} =
135 tr.close()
136
137proc frq_conn_recv*(): cstring {.exportc, dynlib.} =
138 ## The next line, or null when there is none waiting. Never blocks.
139 let (ok, line) = tr.tryLine()
140 if ok: dup(line) else: nil
141
142proc frq_conn_event*(): cstring {.exportc, dynlib.} =
143 ## The next transport event — "open", "close: …", "error: …" — or null.
144 let (ok, e) = tr.tryEvent()
145 if ok: dup(e) else: nil
The renderer, and a transport bug that took four tries 58e6c97 nandi 15h ago146
147
148# ------------------------------------------------------------------- the UI
149#
150# Nim owns the state and the screens; Dart owns the pixels. The only things
151# crossing are a tree going out and an event id coming back.
152
153proc currentTree(): string =
154 ## Whichever screen the state says. `drain` first, so the tree Dart gets is
155 ## built after every line that had arrived when it asked — that is the whole
156 ## of the polling model, and why there is no callback into Dart.
157 drain()
158 let connected = app.status.startsWith("Connected")
159 let node =
160 case app.screen
161 of scChat: scChatScreen.chatScreen(app, connected)
162 of scChats: scChatsScreen.chatsScreen(app, connected)
163 of scDiscover: scSettingsScreen.discoverScreen(app)
164 of scSettings: scSettingsScreen.settingsScreen(app, connected, true)
165 of scConnect: scConnectScreen.connectScreen(app)
166 $node.toJson
167
168proc frq_ui_render*(): cstring {.exportc, dynlib.} =
169 ## The current screen as a widget tree, in JSON.
170 ##
171 ## Not pure: it drains the socket's queue first, so two calls with no
172 ## dispatch between can differ when a line arrived in the gap. That is how
173 ## the room fills, and it is why the renderer polls.
174 dup(currentTree())
175
176proc frq_ui_dispatch*(event: cstring): cstring {.exportc, dynlib.} =
177 ## Apply an event and answer with the tree it produced.
178 ##
179 ## One call rather than dispatch-then-render, and not to save a crossing: it
180 ## makes the pair atomic, so there is no window in which Dart could render a
181 ## state nothing asked for.
182 if event != nil:
183 try:
184 dispatch(parseJson($event))
185 except JsonParsingError:
186 discard
187 dup(currentTree())
188
189proc frq_ui_poll*(): cstring {.exportc, dynlib.} =
190 ## The tree, for a renderer asking because time passed rather than because
191 ## anything happened. Same work as render; named for what the caller means.
192 dup(currentTree())
193
194proc frq_ui_reset*() {.exportc, dynlib.} =
195 tr.close()
196 app = initState()