nandi/frqpublic Fork 0
1d62d1a437b844c37e3e4d49efa7e255ef4deac7
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 · 159 lines · 6.3 KBNim Blame HistoryRaw
One frontend where there were three, and a core that is not Clojure 438b247 nandi 21h 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.
22
23import std/json
A message in #test, from Nim 35994d4 nandi 20h ago24import frq/[ircparse, ui, state, irc]
Nim owns the screen, Dart owns the pixels 43a02c2 nandi 20h ago25import frq/screens/connect as connectScreen
A message in #test, from Nim 35994d4 nandi 20h ago26import frq/screens/chat as chatScreen
One frontend where there were three, and a core that is not Clojure 438b247 nandi 21h ago27
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 20h ago99
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 20h ago106proc currentTree(): string =
The window connects 1d62d1a nandi 20h ago107 maybeAutoconnect()
A message in #test, from Nim 35994d4 nandi 20h ago108 ## Whichever screen the state says. `drain` first, so the tree Dart gets is
109 ## built after every line that had arrived when it asked — that is the whole
110 ## of the polling model, and it is why there is no callback into Dart.
111 drain()
112 case app.screen
113 of scChat: $chatScreen.chatScreen(app).toJson
114 else: $connectScreen.connectScreen(app).toJson
115
Nim owns the screen, Dart owns the pixels 43a02c2 nandi 20h ago116proc frq_ui_render*(): cstring {.exportc, dynlib.} =
A message in #test, from Nim 35994d4 nandi 20h ago117 ## The current screen as a widget tree, in JSON.
118 ##
119 ## No longer pure, and the change is worth naming: it drains the socket's
120 ## queue first, so two calls with no dispatch between can differ when a line
121 ## arrived in the gap. That is the point — it is how the room fills — but it
122 ## means the renderer must be free to call this whenever it likes, which is
123 ## what the Dart side's poll timer does.
124 dup(currentTree())
Nim owns the screen, Dart owns the pixels 43a02c2 nandi 20h ago125
126proc frq_ui_dispatch*(event: cstring): cstring {.exportc, dynlib.} =
127 ## Apply an event and answer with the tree it produced.
128 ##
129 ## One call rather than dispatch-then-render, and not to save a crossing:
130 ## it makes the pair atomic. Two calls leave a window in which Dart could
131 ## render a state nothing asked for, which is the sort of thing that shows
132 ## up once a week and never in a test.
133 ##
134 ## A malformed event is ignored rather than fatal — it arrives from a tree
135 ## the renderer may have been holding for a frame, which is a normal race.
136 if event != nil:
137 try:
138 dispatch(parseJson($event))
139 except JsonParsingError:
140 discard
A message in #test, from Nim 35994d4 nandi 20h ago141 dup(currentTree())
142
143proc frq_ui_poll*(): cstring {.exportc, dynlib.} =
144 ## The tree, for a renderer that is asking because time passed rather than
145 ## because anything happened. Identical to `frq_ui_render` — named
146 ## separately so the Dart side reads as what it means.
147 dup(currentTree())
148
149proc frq_ui_offline*() {.exportc, dynlib.} =
150 ## Stop `connect` from opening a socket, for a test that wants the screens
151 ## without the network. There is no way back — a process that has asked for
152 ## this is a test process.
153 goOffline()
Nim owns the screen, Dart owns the pixels 43a02c2 nandi 20h ago154
155proc frq_ui_reset*() {.exportc, dynlib.} =
156 ## Back to a fresh state. For tests, and for a renderer that wants a known
157 ## starting point rather than whatever the last run left.
A message in #test, from Nim 35994d4 nandi 20h ago158 irc.stop()
Nim owns the screen, Dart owns the pixels 43a02c2 nandi 20h ago159 app = initState()