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.dart · 286 lines · 10.7 KBDart Blame HistoryRaw
The binding is Dart, and it works f7aea3b nandi yesterday1/// The Nim core, as Dart functions.
2///
3/// This is the whole of what knows `libfrqcore.so` is a native library; see
4/// `nim/README.md` for why the logic is there rather than under `common/`.
5///
6/// **Dart and not ClojureDart, on purpose.** The point of the Nim core is to
7/// have less Clojure, so new code on this side of the boundary is written in
8/// the language the platform speaks. It also sidesteps a real problem:
9/// `lookupFunction` takes two type arguments, and generic interop is the part
10/// of ClojureDart least worth fighting for a file that is pure marshalling.
11///
12/// No `package:ffi` either. That package exists mostly for `Utf8`
13/// conversions, and doing them here against `dart:convert` costs about ten
14/// lines and keeps `pubspec.yaml` unchanged — which matters because every
15/// dependency added here has to work on three targets.
16///
17/// The two rules of the ABI, wrapped so no call site repeats them:
18///
19/// * `frq_init` runs once before anything else. [_lib] does it on the way
20/// out, so holding the handle means it has happened.
21/// * Every string the core returns is **ours to free**, with `frq_free`.
22/// [_takeString] is that, in a `finally` so a throw between the read and
23/// the free does not leak. Nim's allocator is not Dart's, so calling
24/// `malloc.free` on one of these pointers is undefined rather than merely
25/// untidy.
26library;
27
28import 'dart:convert';
29import 'dart:ffi';
30import 'dart:io';
31
32// ---------------------------------------------------------------- the ABI
33
34typedef _InitNative = Void Function();
35typedef _InitDart = void Function();
36
37typedef _FreeNative = Void Function(Pointer<Uint8>);
38typedef _FreeDart = void Function(Pointer<Uint8>);
39
40typedef _VersionNative = Pointer<Uint8> Function();
41typedef _VersionDart = Pointer<Uint8> Function();
42
43typedef _Str1Native = Pointer<Uint8> Function(Pointer<Uint8>);
44typedef _Str1Dart = Pointer<Uint8> Function(Pointer<Uint8>);
45
46typedef _Str2Native = Pointer<Uint8> Function(Pointer<Uint8>, Pointer<Uint8>);
47typedef _Str2Dart = Pointer<Uint8> Function(Pointer<Uint8>, Pointer<Uint8>);
48
Nim owns the screen, Dart owns the pixels 43a02c2 nandi yesterday49typedef _Str0Native = Pointer<Uint8> Function();
50typedef _Str0Dart = Pointer<Uint8> Function();
51
52typedef _VoidNative = Void Function();
53typedef _VoidDart = void Function();
54
The binding is Dart, and it works f7aea3b nandi yesterday55/// Where to look for the library, in order.
56///
57/// Android resolves a bare soname out of the APK's `lib/<abi>/`. A desktop
58/// build has no such rule, so the bare name is tried first (it works when the
59/// object sits beside the executable or on the loader path) and then the
60/// development path `just nim-lib` writes to. Named explicitly rather than by
61/// exporting `LD_LIBRARY_PATH` from a launcher, because a variable set in a
62/// wrapper script is a thing that works until someone starts the binary
63/// another way.
64DynamicLibrary _open() {
65 if (Platform.isAndroid) return DynamicLibrary.open('libfrqcore.so');
66 // The development paths are relative to whichever directory the process
67 // started in: `dart test` runs from `dart/frq_core`, a built desktop bundle
68 // from the repo root. All of them are tried rather than guessing which
69 // invocation this is, because the failure mode is a StateError at first use
70 // rather than anything a type checker would have caught.
71 for (final p in [
72 'libfrqcore.so',
73 'build/nim/libfrqcore.so',
Nim owns the screen, Dart owns the pixels 43a02c2 nandi yesterday74 '../build/nim/libfrqcore.so', // `flutter test`, from flutter/
75 '../../build/nim/libfrqcore.so', // `dart test`, from dart/frq_core/
The binding is Dart, and it works f7aea3b nandi yesterday76 ]) {
77 try {
78 return DynamicLibrary.open(p);
79 } on ArgumentError {
80 continue;
81 }
82 }
83 throw StateError(
84 'libfrqcore.so not found — build it with `just nim-lib`, or ship it '
85 'beside the executable');
86}
87
88final DynamicLibrary _lib = () {
89 final lib = _open();
90 lib.lookupFunction<_InitNative, _InitDart>('frq_init')();
91 return lib;
92}();
93
94final _free = _lib.lookupFunction<_FreeNative, _FreeDart>('frq_free');
95
96/// The bytes at [p] as a string, with [p] freed afterwards. Null in, null out.
97String? _takeString(Pointer<Uint8> p) {
98 if (p == nullptr) return null;
99 try {
100 // Walk to the NUL rather than asking for a length the ABI does not carry.
101 var len = 0;
102 while (p[len] != 0) {
103 len++;
104 }
105 return utf8.decode(p.asTypedList(len));
106 } finally {
107 _free(p);
108 }
109}
110
111/// [s] as a NUL-terminated C string that the CALLER must free with [_freeArg].
112///
113/// Allocated with `malloc` from Dart's side, so it is freed from Dart's side —
114/// the mirror of the rule for what comes back. The core never takes ownership
115/// of an argument.
116Pointer<Uint8> _toC(String s) {
117 final bytes = utf8.encode(s);
118 final p = _malloc(bytes.length + 1).cast<Uint8>();
119 for (var i = 0; i < bytes.length; i++) {
120 p[i] = bytes[i];
121 }
122 p[bytes.length] = 0;
123 return p;
124}
125
126// malloc/free out of libc rather than package:ffi's allocator, for the same
127// reason the rest of this file avoids that package: one less dependency to
128// carry to three targets, for two symbols that are always there.
129final DynamicLibrary _libc =
130 Platform.isWindows ? DynamicLibrary.open('msvcrt.dll') : DynamicLibrary.process();
131final _malloc = _libc
132 .lookupFunction<Pointer<Void> Function(IntPtr), Pointer<Void> Function(int)>('malloc');
133final _freeArg =
134 _libc.lookupFunction<Void Function(Pointer<Uint8>), void Function(Pointer<Uint8>)>('free');
135
136/// Call a one-string-in, one-string-out entry point.
137String? _call1(String symbol, String arg) {
138 final f = _lib.lookupFunction<_Str1Native, _Str1Dart>(symbol);
139 final a = _toC(arg);
140 try {
141 return _takeString(f(a));
142 } finally {
143 _freeArg(a);
144 }
145}
146
147// ------------------------------------------------------------------ public
148
149/// The core's version, for a caller that wants to check the library it found
150/// is the one it was built against. Static storage on the Nim side: the one
151/// return value that is NOT freed.
152String get version {
153 final p = _lib.lookupFunction<_VersionNative, _VersionDart>('frq_version')();
154 var len = 0;
155 while (p[len] != 0) {
156 len++;
157 }
158 return utf8.decode(p.asTypedList(len));
159}
160
161/// An IRC line, taken apart: `{raw, tags, account, prefix, command, params}`.
162///
163/// `tags`, `account` and `prefix` are null where the line carried none, which
164/// is the distinction `frq.irc.parse` draws with nil and every caller depends
165/// on — a PRIVMSG from a server with no prefix is not the same line as one
166/// from a nick.
167Map<String, dynamic> parseLine(String line) {
168 final json = _call1('frq_irc_parse_line', line);
169 return jsonDecode(json ?? 'null') as Map<String, dynamic>;
170}
171
172/// One IRCv3 tag's value, unescaped — null where the tag is absent OR empty,
173/// which IRCv3 says are the same thing.
174String? tagValue(String tags, String key) {
175 final f = _lib.lookupFunction<_Str2Native, _Str2Dart>('frq_irc_tag_value');
176 final a = _toC(tags);
177 final b = _toC(key);
178 try {
179 return _takeString(f(a, b));
180 } finally {
181 _freeArg(a);
182 _freeArg(b);
183 }
184}
185
186String unescapeTag(String v) => _call1('frq_irc_unescape_tag', v) ?? '';
187
188String escapeTagValue(String v) => _call1('frq_irc_escape_tag_value', v) ?? '';
189
190/// The nick half of a `nick!user@host` prefix.
191String nickOf(String prefix) => _call1('frq_irc_nick_of', prefix) ?? '';
Nim owns the screen, Dart owns the pixels 43a02c2 nandi yesterday192
193
194// ---------------------------------------------------------------- the UI
195//
196// The spike's claim: Nim owns the state and the screen, Dart owns the pixels.
197// A tree goes out, an event id comes back, and nothing else crosses.
198//
199// `UiNode` is deliberately a dumb bag — a tag, a props map, children. Giving
200// it a class per widget would put the tag vocabulary in two places and make
201// every new tag a change on both sides of the boundary; the whole point is
202// that Nim can grow a screen without Dart being recompiled.
203
204/// One node of the widget tree Nim emitted.
205class UiNode {
206 final String tag;
207 final Map<String, dynamic> props;
208 final List<UiNode> children;
209
210 const UiNode(this.tag, this.props, this.children);
211
212 factory UiNode.fromJson(Map<String, dynamic> j) => UiNode(
213 j['tag'] as String,
214 (j['props'] as Map?)?.cast<String, dynamic>() ?? const {},
215 ((j['children'] as List?) ?? const [])
216 .map((c) => UiNode.fromJson((c as Map).cast<String, dynamic>()))
217 .toList(growable: false),
218 );
219
220 /// A prop, or [fallback] when it is absent or the wrong shape. Tolerant on
221 /// purpose: the renderer should skip a prop it does not understand rather
222 /// than fail a whole screen over one.
223 T prop<T>(String name, T fallback) {
224 final v = props[name];
225 return v is T ? v : fallback;
226 }
227
A message in #test, from Nim 35994d4 nandi yesterday228 /// Structural, and that matters: the poll loop compares two trees by this
229 /// string to decide whether to rebuild. A summary that showed only the tag
230 /// and the prop NAMES would call two screens equal when a message had
231 /// arrived, and the room would never appear to fill.
Nim owns the screen, Dart owns the pixels 43a02c2 nandi yesterday232 @override
A message in #test, from Nim 35994d4 nandi yesterday233 String toString() =>
234 '<$tag $props ${children.map((c) => c.toString()).join()}>';
Nim owns the screen, Dart owns the pixels 43a02c2 nandi yesterday235}
236
A message in #test, from Nim 35994d4 nandi yesterday237/// The current screen.
238///
239/// Not pure: the Nim side drains the socket's queue first, so two calls with
240/// no [dispatch] between can differ when a line arrived in the gap. That is
241/// how the room fills, and it is why the renderer polls.
Nim owns the screen, Dart owns the pixels 43a02c2 nandi yesterday242UiNode render() {
243 final f = _lib.lookupFunction<_Str0Native, _Str0Dart>('frq_ui_render');
244 final json = _takeString(f());
245 return UiNode.fromJson(jsonDecode(json!) as Map<String, dynamic>);
246}
247
248/// Apply an event and get the tree it produced.
249///
250/// One call rather than dispatch-then-render, and not to save a crossing: it
251/// makes the pair atomic, so there is no window in which Dart could render a
252/// state nothing asked for.
253UiNode dispatch(String id, [String value = '']) {
254 final f = _lib.lookupFunction<_Str1Native, _Str1Dart>('frq_ui_dispatch');
255 final a = _toC(jsonEncode({'id': id, 'value': value}));
256 try {
257 final json = _takeString(f(a));
258 return UiNode.fromJson(jsonDecode(json!) as Map<String, dynamic>);
259 } finally {
260 _freeArg(a);
261 }
262}
263
A message in #test, from Nim 35994d4 nandi yesterday264/// The tree, asked for because time passed rather than because anything
265/// happened.
266///
267/// Identical to [render] — the Nim side drains the socket queue on both — but
268/// named for what the caller means. A renderer polls this; it does not poll
269/// "render".
270UiNode poll() {
271 final f = _lib.lookupFunction<_Str0Native, _Str0Dart>('frq_ui_poll');
272 final json = _takeString(f());
273 return UiNode.fromJson(jsonDecode(json!) as Map<String, dynamic>);
274}
275
276/// Stop `connect` from opening a socket.
277///
278/// For tests that build the real screens and tap the real Connect button. A
279/// widget test that dials irc.freeq.at is one that fails on a train, and this
280/// suite did exactly that before this existed. One way only.
281void goOffline() =>
282 _lib.lookupFunction<_VoidNative, _VoidDart>('frq_ui_offline')();
283
Nim owns the screen, Dart owns the pixels 43a02c2 nandi yesterday284/// Back to a fresh state, for a caller that wants a known starting point.
285void resetUi() =>
286 _lib.lookupFunction<_VoidNative, _VoidDart>('frq_ui_reset')();