| A web version, from the same core 23846db nandi 13h ago | 1 | /// The native half of the seam: `libfrqcore.so`, through `dart:ffi`. |
| 2 | /// |
| 3 | /// Split out of `frq_core.dart` so that file can be imported where there is |
| 4 | /// no `dart:ffi` to import — a browser. `host.dart` picks this or `host_js` |
| 5 | /// and nothing above it knows which. |
| 6 | /// |
| 7 | /// The two rules of the ABI live here, wrapped so no call site repeats them: |
| 8 | /// `frq_init` runs once before anything else, and every string the core |
| 9 | /// returns is ours to free with `frq_free`. |
| 10 | library; |
| 11 | |
| 12 | import 'dart:convert'; |
| 13 | import 'dart:ffi'; |
| 14 | import 'dart:io'; |
| 15 | |
| 16 | // ---------------------------------------------------------------- the ABI |
| 17 | |
| 18 | typedef _InitNative = Void Function(); |
| 19 | typedef _InitDart = void Function(); |
| 20 | |
| 21 | typedef _FreeNative = Void Function(Pointer<Uint8>); |
| 22 | typedef _FreeDart = void Function(Pointer<Uint8>); |
| 23 | |
| 24 | typedef _VersionNative = Pointer<Uint8> Function(); |
| 25 | typedef _VersionDart = Pointer<Uint8> Function(); |
| 26 | |
| 27 | typedef _Str1Native = Pointer<Uint8> Function(Pointer<Uint8>); |
| 28 | typedef _Str1Dart = Pointer<Uint8> Function(Pointer<Uint8>); |
| 29 | |
| 30 | typedef _Str2Native = Pointer<Uint8> Function(Pointer<Uint8>, Pointer<Uint8>); |
| 31 | typedef _Str2Dart = Pointer<Uint8> Function(Pointer<Uint8>, Pointer<Uint8>); |
| 32 | |
| 33 | typedef _Str0Native = Pointer<Uint8> Function(); |
| 34 | typedef _Str0Dart = Pointer<Uint8> Function(); |
| 35 | |
| 36 | typedef _VoidNative = Void Function(); |
| 37 | typedef _VoidDart = void Function(); |
| 38 | |
| 39 | /// Where to look for the library, in order. |
| 40 | /// |
| 41 | /// Android resolves a bare soname out of the APK's `lib/<abi>/`. A desktop |
| 42 | /// build has no such rule, so the bare name is tried first (it works when the |
| 43 | /// object sits beside the executable or on the loader path) and then the |
| 44 | /// development path `just nim-lib` writes to. Named explicitly rather than by |
| 45 | /// exporting `LD_LIBRARY_PATH` from a launcher, because a variable set in a |
| 46 | /// wrapper script is a thing that works until someone starts the binary |
| 47 | /// another way. |
| 48 | DynamicLibrary _open() { |
| 49 | if (Platform.isAndroid) return DynamicLibrary.open('libfrqcore.so'); |
| 50 | // The development paths are relative to whichever directory the process |
| 51 | // started in: `dart test` runs from `dart/frq_core`, a built desktop bundle |
| 52 | // from the repo root. All of them are tried rather than guessing which |
| 53 | // invocation this is, because the failure mode is a StateError at first use |
| 54 | // rather than anything a type checker would have caught. |
| 55 | for (final p in [ |
| 56 | 'libfrqcore.so', |
| 57 | 'build/nim/libfrqcore.so', |
| 58 | '../build/nim/libfrqcore.so', // `flutter test`, from flutter/ |
| 59 | '../../build/nim/libfrqcore.so', // `dart test`, from dart/frq_core/ |
| 60 | ]) { |
| 61 | try { |
| 62 | return DynamicLibrary.open(p); |
| 63 | } on ArgumentError { |
| 64 | continue; |
| 65 | } |
| 66 | } |
| 67 | throw StateError( |
| 68 | 'libfrqcore.so not found — build it with `just nim-lib`, or ship it ' |
| 69 | 'beside the executable'); |
| 70 | } |
| 71 | |
| 72 | final DynamicLibrary _lib = () { |
| 73 | final lib = _open(); |
| 74 | lib.lookupFunction<_InitNative, _InitDart>('frq_init')(); |
| 75 | return lib; |
| 76 | }(); |
| 77 | |
| 78 | // Every entry point resolved once, here, rather than on each call. |
| 79 | // |
| 80 | // `lookupFunction` is a dlsym plus a freshly built trampoline closure every |
| 81 | // time it runs. At 10Hz for `poll` and once per keystroke for `dispatch` that |
| 82 | // is measurable and, more to the point, free to avoid — these are `final`, so |
| 83 | // they cost one lookup for the life of the process. |
| 84 | final _free = _lib.lookupFunction<_FreeNative, _FreeDart>('frq_free'); |
| 85 | final _version = _lib.lookupFunction<_VersionNative, _VersionDart>('frq_version'); |
| 86 | final _tagValue = _lib.lookupFunction<_Str2Native, _Str2Dart>('frq_irc_tag_value'); |
| 87 | final _traceFn = _lib.lookupFunction<_Str2Native, _Str2Dart>('frq_trace'); |
| 88 | final _connOpen = _lib.lookupFunction<_ConnOpenNative, _ConnOpenDart>('frq_conn_open'); |
| 89 | final _connSend = _lib.lookupFunction<_Str1Native, _Str1Dart>('frq_conn_send'); |
| 90 | final _connCloseFn = _lib.lookupFunction<_VoidNative, _VoidDart>('frq_conn_close'); |
| 91 | final _connRecvFn = _lib.lookupFunction<_Str0Native, _Str0Dart>('frq_conn_recv'); |
| 92 | final _connEventFn = _lib.lookupFunction<_Str0Native, _Str0Dart>('frq_conn_event'); |
| 93 | final _uiRender = _lib.lookupFunction<_Str0Native, _Str0Dart>('frq_ui_render'); |
| 94 | final _uiPoll = _lib.lookupFunction<_Str0Native, _Str0Dart>('frq_ui_poll'); |
| 95 | final _uiDispatch = _lib.lookupFunction<_Str1Native, _Str1Dart>('frq_ui_dispatch'); |
| 96 | final _uiDemo = _lib.lookupFunction<_VoidNative, _VoidDart>('frq_ui_demo'); |
| 97 | final _uiReset = _lib.lookupFunction<_VoidNative, _VoidDart>('frq_ui_reset'); |
| 98 | final _str1 = <String, _Str1Dart>{}; |
| 99 | |
| 100 | /// The bytes at [p] as a string, with [p] freed afterwards. Null in, null out. |
| 101 | String? _takeString(Pointer<Uint8> p) { |
| 102 | if (p == nullptr) return null; |
| 103 | try { |
| 104 | // Walk to the NUL rather than asking for a length the ABI does not carry. |
| 105 | var len = 0; |
| 106 | while (p[len] != 0) { |
| 107 | len++; |
| 108 | } |
| 109 | return utf8.decode(p.asTypedList(len)); |
| 110 | } finally { |
| 111 | _free(p); |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | /// [s] as a NUL-terminated C string that the CALLER must free with [_freeArg]. |
| 116 | /// |
| 117 | /// Allocated with `malloc` from Dart's side, so it is freed from Dart's side — |
| 118 | /// the mirror of the rule for what comes back. The core never takes ownership |
| 119 | /// of an argument. |
| 120 | Pointer<Uint8> _toC(String s) { |
| 121 | final bytes = utf8.encode(s); |
| 122 | final p = _malloc(bytes.length + 1).cast<Uint8>(); |
| 123 | for (var i = 0; i < bytes.length; i++) { |
| 124 | p[i] = bytes[i]; |
| 125 | } |
| 126 | p[bytes.length] = 0; |
| 127 | return p; |
| 128 | } |
| 129 | |
| 130 | // malloc/free out of libc rather than package:ffi's allocator, for the same |
| 131 | // reason the rest of this file avoids that package: one less dependency to |
| 132 | // carry to three targets, for two symbols that are always there. |
| 133 | final DynamicLibrary _libc = |
| 134 | Platform.isWindows ? DynamicLibrary.open('msvcrt.dll') : DynamicLibrary.process(); |
| 135 | final _malloc = _libc |
| 136 | .lookupFunction<Pointer<Void> Function(IntPtr), Pointer<Void> Function(int)>('malloc'); |
| 137 | final _freeArg = |
| 138 | _libc.lookupFunction<Void Function(Pointer<Uint8>), void Function(Pointer<Uint8>)>('free'); |
| 139 | |
| 140 | /// Call a two-strings-in, one-string-out entry point. |
| 141 | /// |
| 142 | /// The mirror of [_call1], and it exists for the same reason: the |
| 143 | /// `_toC`/`try`/`finally`/`_freeArg` dance is four lines of ownership |
| 144 | /// bookkeeping that no call site should repeat. |
| 145 | String? _call2(_Str2Dart f, String x, String y) { |
| 146 | final a = _toC(x); |
| 147 | final b = _toC(y); |
| 148 | try { |
| 149 | return _takeString(f(a, b)); |
| 150 | } finally { |
| 151 | _freeArg(a); |
| 152 | _freeArg(b); |
| 153 | } |
| 154 | } |
| 155 | |
| 156 | /// Call a one-string-in, one-string-out entry point. |
| 157 | String? _call1(String symbol, String arg) { |
| 158 | final f = _str1.putIfAbsent( |
| 159 | symbol, () => _lib.lookupFunction<_Str1Native, _Str1Dart>(symbol)); |
| 160 | final a = _toC(arg); |
| 161 | try { |
| 162 | return _takeString(f(a)); |
| 163 | } finally { |
| 164 | _freeArg(a); |
| 165 | } |
| 166 | } |
| 167 | |
| 168 | |
| 169 | typedef _ConnOpenNative = Void Function(Pointer<Uint8>, Int32, Int32); |
| 170 | typedef _ConnOpenDart = void Function(Pointer<Uint8>, int, int); |
| 171 | |
| 172 | // ------------------------------------------------------------------ the host |
| 173 | // |
| 174 | // What `frq_core.dart` calls, and what `host_js.dart` answers with the same |
| 175 | // names. Everything above is how; this is what. |
| 176 | |
| 177 | String? uiRender() => _takeString(_uiRender()); |
| 178 | String? uiPoll() => _takeString(_uiPoll()); |
| 179 | |
| 180 | String? uiDispatch(String event) { |
| 181 | final a = _toC(event); |
| 182 | try { |
| 183 | return _takeString(_uiDispatch(a)); |
| 184 | } finally { |
| 185 | _freeArg(a); |
| 186 | } |
| 187 | } |
| 188 | |
| 189 | void uiDemo() => _uiDemo(); |
| 190 | void uiReset() => _uiReset(); |
| 191 | |
| 192 | /// Static storage on the Nim side: the one return value that is NOT freed. |
| 193 | String hostVersion() { |
| 194 | final p = _version(); |
| 195 | var len = 0; |
| 196 | while (p[len] != 0) { |
| 197 | len++; |
| 198 | } |
| 199 | return utf8.decode(p.asTypedList(len)); |
| 200 | } |
| 201 | |
| 202 | String? str1(String symbol, String arg) => _call1(symbol, arg); |
| 203 | String? tagValueOf(String tags, String key) => _call2(_tagValue, tags, key); |
| 204 | void traceTo(String topic, String msg) { |
| 205 | _call2(_traceFn, topic, msg); |
| 206 | } |
| 207 | |
| 208 | void connOpenAt(String host, int port, bool tls) { |
| 209 | final h = _toC(host); |
| 210 | try { |
| 211 | _connOpen(h, port, tls ? 1 : 0); |
| 212 | } finally { |
| 213 | _freeArg(h); |
| 214 | } |
| 215 | } |
| 216 | |
| 217 | void connSendLine(String line) { |
| 218 | final a = _toC(line); |
| 219 | try { |
| 220 | _connSend(a); |
| 221 | } finally { |
| 222 | _freeArg(a); |
| 223 | } |
| 224 | } |
| 225 | |
| 226 | void connCloseNow() => _connCloseFn(); |
| 227 | String? connRecvLine() => _takeString(_connRecvFn()); |
| 228 | String? connEventNext() => _takeString(_connEventFn()); |