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
|
## 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.
import std/json
import frq/[ircparse, ui, state, irc]
import frq/screens/connect as connectScreen
import frq/screens/chat as chatScreen
proc NimMain() {.importc.}
var initialised = false
proc frq_init*() {.exportc, dynlib.} =
## Set Nim's runtime up. Idempotent, because a binding that guesses wrong
## about whether it has been called should be harmless rather than fatal.
if not initialised:
NimMain()
initialised = true
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))
# ------------------------------------------------------------------- the UI
#
# The spike's real claim: Nim owns the state and the screen, Dart owns the
# pixels, and the only things crossing are a tree going out and an event id
# coming back. See `frq/ui.nim`.
proc currentTree(): string =
maybeAutoconnect()
## 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 it is why there is no callback into Dart.
drain()
case app.screen
of scChat: $chatScreen.chatScreen(app).toJson
else: $connectScreen.connectScreen(app).toJson
proc frq_ui_render*(): cstring {.exportc, dynlib.} =
## The current screen as a widget tree, in JSON.
##
## No longer pure, and the change is worth naming: 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 the point — it is how the room fills — but it
## means the renderer must be free to call this whenever it likes, which is
## what the Dart side's poll timer does.
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. Two calls leave a window in which Dart could
## render a state nothing asked for, which is the sort of thing that shows
## up once a week and never in a test.
##
## A malformed event is ignored rather than fatal — it arrives from a tree
## the renderer may have been holding for a frame, which is a normal race.
if event != nil:
try:
dispatch(parseJson($event))
except JsonParsingError:
discard
dup(currentTree())
proc frq_ui_poll*(): cstring {.exportc, dynlib.} =
## The tree, for a renderer that is asking because time passed rather than
## because anything happened. Identical to `frq_ui_render` — named
## separately so the Dart side reads as what it means.
dup(currentTree())
proc frq_ui_offline*() {.exportc, dynlib.} =
## Stop `connect` from opening a socket, for a test that wants the screens
## without the network. There is no way back — a process that has asked for
## this is a test process.
goOffline()
proc frq_ui_reset*() {.exportc, dynlib.} =
## Back to a fresh state. For tests, and for a renderer that wants a known
## starting point rather than whatever the last run left.
irc.stop()
app = initState()
|