nandi/frqpublic Fork 0
f7aea3bb50324a5898f663f4a2f1173271fc7e3c
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 · 184 lines · 6.8 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
49/// Where to look for the library, in order.
50///
51/// Android resolves a bare soname out of the APK's `lib/<abi>/`. A desktop
52/// build has no such rule, so the bare name is tried first (it works when the
53/// object sits beside the executable or on the loader path) and then the
54/// development path `just nim-lib` writes to. Named explicitly rather than by
55/// exporting `LD_LIBRARY_PATH` from a launcher, because a variable set in a
56/// wrapper script is a thing that works until someone starts the binary
57/// another way.
58DynamicLibrary _open() {
59 if (Platform.isAndroid) return DynamicLibrary.open('libfrqcore.so');
60 // The development paths are relative to whichever directory the process
61 // started in: `dart test` runs from `dart/frq_core`, a built desktop bundle
62 // from the repo root. All of them are tried rather than guessing which
63 // invocation this is, because the failure mode is a StateError at first use
64 // rather than anything a type checker would have caught.
65 for (final p in [
66 'libfrqcore.so',
67 'build/nim/libfrqcore.so',
68 '../../build/nim/libfrqcore.so',
69 ]) {
70 try {
71 return DynamicLibrary.open(p);
72 } on ArgumentError {
73 continue;
74 }
75 }
76 throw StateError(
77 'libfrqcore.so not found — build it with `just nim-lib`, or ship it '
78 'beside the executable');
79}
80
81final DynamicLibrary _lib = () {
82 final lib = _open();
83 lib.lookupFunction<_InitNative, _InitDart>('frq_init')();
84 return lib;
85}();
86
87final _free = _lib.lookupFunction<_FreeNative, _FreeDart>('frq_free');
88
89/// The bytes at [p] as a string, with [p] freed afterwards. Null in, null out.
90String? _takeString(Pointer<Uint8> p) {
91 if (p == nullptr) return null;
92 try {
93 // Walk to the NUL rather than asking for a length the ABI does not carry.
94 var len = 0;
95 while (p[len] != 0) {
96 len++;
97 }
98 return utf8.decode(p.asTypedList(len));
99 } finally {
100 _free(p);
101 }
102}
103
104/// [s] as a NUL-terminated C string that the CALLER must free with [_freeArg].
105///
106/// Allocated with `malloc` from Dart's side, so it is freed from Dart's side —
107/// the mirror of the rule for what comes back. The core never takes ownership
108/// of an argument.
109Pointer<Uint8> _toC(String s) {
110 final bytes = utf8.encode(s);
111 final p = _malloc(bytes.length + 1).cast<Uint8>();
112 for (var i = 0; i < bytes.length; i++) {
113 p[i] = bytes[i];
114 }
115 p[bytes.length] = 0;
116 return p;
117}
118
119// malloc/free out of libc rather than package:ffi's allocator, for the same
120// reason the rest of this file avoids that package: one less dependency to
121// carry to three targets, for two symbols that are always there.
122final DynamicLibrary _libc =
123 Platform.isWindows ? DynamicLibrary.open('msvcrt.dll') : DynamicLibrary.process();
124final _malloc = _libc
125 .lookupFunction<Pointer<Void> Function(IntPtr), Pointer<Void> Function(int)>('malloc');
126final _freeArg =
127 _libc.lookupFunction<Void Function(Pointer<Uint8>), void Function(Pointer<Uint8>)>('free');
128
129/// Call a one-string-in, one-string-out entry point.
130String? _call1(String symbol, String arg) {
131 final f = _lib.lookupFunction<_Str1Native, _Str1Dart>(symbol);
132 final a = _toC(arg);
133 try {
134 return _takeString(f(a));
135 } finally {
136 _freeArg(a);
137 }
138}
139
140// ------------------------------------------------------------------ public
141
142/// The core's version, for a caller that wants to check the library it found
143/// is the one it was built against. Static storage on the Nim side: the one
144/// return value that is NOT freed.
145String get version {
146 final p = _lib.lookupFunction<_VersionNative, _VersionDart>('frq_version')();
147 var len = 0;
148 while (p[len] != 0) {
149 len++;
150 }
151 return utf8.decode(p.asTypedList(len));
152}
153
154/// An IRC line, taken apart: `{raw, tags, account, prefix, command, params}`.
155///
156/// `tags`, `account` and `prefix` are null where the line carried none, which
157/// is the distinction `frq.irc.parse` draws with nil and every caller depends
158/// on — a PRIVMSG from a server with no prefix is not the same line as one
159/// from a nick.
160Map<String, dynamic> parseLine(String line) {
161 final json = _call1('frq_irc_parse_line', line);
162 return jsonDecode(json ?? 'null') as Map<String, dynamic>;
163}
164
165/// One IRCv3 tag's value, unescaped — null where the tag is absent OR empty,
166/// which IRCv3 says are the same thing.
167String? tagValue(String tags, String key) {
168 final f = _lib.lookupFunction<_Str2Native, _Str2Dart>('frq_irc_tag_value');
169 final a = _toC(tags);
170 final b = _toC(key);
171 try {
172 return _takeString(f(a, b));
173 } finally {
174 _freeArg(a);
175 _freeArg(b);
176 }
177}
178
179String unescapeTag(String v) => _call1('frq_irc_unescape_tag', v) ?? '';
180
181String escapeTagValue(String v) => _call1('frq_irc_escape_tag_value', v) ?? '';
182
183/// The nick half of a `nick!user@host` prefix.
184String nickOf(String prefix) => _call1('frq_irc_nick_of', prefix) ?? '';