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