Use linkme, not macro expansion, for Native.ts generation
This commit is contained in:
parent
7543c3d35b
commit
4d43a6270a
2
.github/workflows/build_and_test.yml
vendored
2
.github/workflows/build_and_test.yml
vendored
@ -589,7 +589,7 @@ jobs:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Verify that the Node bindings are up to date
|
||||
run: rust/bridge/node/bin/gen_ts_decl.py --verify
|
||||
run: cargo run -p libsignal-node-native_ts -- --verify
|
||||
if: startsWith(matrix.os, 'ubuntu-')
|
||||
|
||||
- run: npm ci
|
||||
|
||||
2
.github/workflows/npm.yml
vendored
2
.github/workflows/npm.yml
vendored
@ -154,7 +154,7 @@ jobs:
|
||||
- run: sudo apt-get install -U protobuf-compiler
|
||||
|
||||
- name: Verify that the Node bindings are up to date
|
||||
run: rust/bridge/node/bin/gen_ts_decl.py --verify
|
||||
run: cargo run -p libsignal-node-native_ts -- --verify
|
||||
|
||||
publish:
|
||||
name: Publish
|
||||
|
||||
31
Cargo.lock
generated
31
Cargo.lock
generated
@ -2992,6 +2992,7 @@ dependencies = [
|
||||
"libsignal-bridge",
|
||||
"libsignal-bridge-macros",
|
||||
"libsignal-bridge-testing",
|
||||
"libsignal-bridge-types",
|
||||
"libsignal-protocol",
|
||||
"linkme",
|
||||
"log",
|
||||
@ -3005,6 +3006,19 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsignal-node-native_ts"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"libsignal-bridge",
|
||||
"libsignal-bridge-testing",
|
||||
"libsignal-bridge-types",
|
||||
"libsignal-node",
|
||||
"minijinja",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsignal-protocol"
|
||||
version = "0.1.0"
|
||||
@ -3206,6 +3220,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memo-map"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38d1115007560874e373613744c6fba374c17688327a71c1476d1a5954cc857b"
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
@ -3291,6 +3311,17 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minijinja"
|
||||
version = "2.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "805bfd7352166bae857ee569628b52bcd85a1cecf7810861ebceb1686b72b75d"
|
||||
dependencies = [
|
||||
"indexmap 2.13.0",
|
||||
"memo-map",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
|
||||
@ -22,6 +22,7 @@ members = [
|
||||
"rust/bridge/jni/impl",
|
||||
"rust/bridge/jni/testing",
|
||||
"rust/bridge/node",
|
||||
"rust/bridge/node/native_ts",
|
||||
]
|
||||
default-members = [
|
||||
"rust/crypto",
|
||||
@ -67,6 +68,7 @@ libsignal-message-backup = { path = "rust/message-backup" }
|
||||
libsignal-net = { path = "rust/net" }
|
||||
libsignal-net-chat = { path = "rust/net/chat" }
|
||||
libsignal-net-grpc = { path = "rust/net/grpc" }
|
||||
libsignal-node = { path = "rust/bridge/node" }
|
||||
libsignal-protocol = { path = "rust/protocol" }
|
||||
libsignal-svrb = { path = "rust/svrb" }
|
||||
poksho = { path = "rust/poksho" }
|
||||
@ -160,6 +162,7 @@ mediasan-common = "0.5.3"
|
||||
minidump = { version = "0.22.1", default-features = false }
|
||||
minidump-processor = { version = "0.22.1", default-features = false }
|
||||
minidump-unwind = { version = "0.22.1", default-features = false }
|
||||
minijinja = "2.19.0"
|
||||
mp4san = "0.5.3"
|
||||
neon = { version = "1.1.0", default-features = false }
|
||||
nonzero_ext = "0.3.0"
|
||||
|
||||
@ -214,7 +214,7 @@ $ npm run test
|
||||
|
||||
When testing changes locally, you can use `npm run build` to do an incremental rebuild of the Rust library. Alternately, `npm run build-with-debug-level-logs` will rebuild without filtering out debug- and verbose-level logs.
|
||||
|
||||
When exposing new APIs to Node, you will need to run `rust/bridge/node/bin/gen_ts_decl.py` in
|
||||
When exposing new APIs to Node, you will need to run `just generate-node` in
|
||||
addition to rebuilding.
|
||||
|
||||
[nvm]: https://github.com/nvm-sh/nvm
|
||||
|
||||
@ -47,8 +47,8 @@
|
||||
<h2>Overview of licenses:</h2>
|
||||
<ul class="licenses-overview">
|
||||
<li><a href="#MIT">MIT License</a> (349)</li>
|
||||
<li><a href="#AGPL-3.0-only">GNU Affero General Public License v3.0 only</a> (36)</li>
|
||||
<li><a href="#Apache-2.0">Apache License 2.0</a> (25)</li>
|
||||
<li><a href="#AGPL-3.0-only">GNU Affero General Public License v3.0 only</a> (37)</li>
|
||||
<li><a href="#Apache-2.0">Apache License 2.0</a> (27)</li>
|
||||
<li><a href="#BSD-3-Clause">BSD 3-Clause "New" or "Revised" License</a> (9)</li>
|
||||
<li><a href="#ISC">ISC License</a> (4)</li>
|
||||
<li><a href="#MPL-2.0">Mozilla Public License 2.0</a> (2)</li>
|
||||
@ -741,6 +741,7 @@ For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<li><a href="https://crates.io/crates/libsignal-node">libsignal-node</a></li>
|
||||
<li><a href="https://crates.io/crates/signal-neon-futures">signal-neon-futures</a></li>
|
||||
<li><a href="https://crates.io/crates/signal-neon-futures-tests">signal-neon-futures-tests</a></li>
|
||||
<li><a href="https://crates.io/crates/libsignal-node-native_ts">libsignal-node-native_ts</a></li>
|
||||
<li><a href="https://crates.io/crates/libsignal-bridge">libsignal-bridge</a></li>
|
||||
<li><a href="https://crates.io/crates/libsignal-bridge-macros">libsignal-bridge-macros</a></li>
|
||||
<li><a href="https://crates.io/crates/libsignal-bridge-testing">libsignal-bridge-testing</a></li>
|
||||
@ -2937,6 +2938,8 @@ END OF TERMS AND CONDITIONS
|
||||
<h4>Used by:</h4>
|
||||
<ul class="license-used-by">
|
||||
<li><a href="https://github.com/getsentry/rust-debugid">debugid 0.8.0</a></li>
|
||||
<li><a href="https://github.com/mitsuhiko/memo-map">memo-map 0.3.3</a></li>
|
||||
<li><a href="https://github.com/mitsuhiko/minijinja">minijinja 2.19.0</a></li>
|
||||
<li><a href="https://github.com/tokio-rs/prost">prost-build 0.14.1</a></li>
|
||||
<li><a href="https://github.com/tokio-rs/prost">prost-derive 0.14.1</a></li>
|
||||
<li><a href="https://github.com/tokio-rs/prost">prost-types 0.14.1</a></li>
|
||||
|
||||
2
justfile
2
justfile
@ -14,7 +14,7 @@ generate-ffi:
|
||||
swift/build_ffi.sh --generate-ffi
|
||||
|
||||
generate-node:
|
||||
rust/bridge/node/bin/gen_ts_decl.py
|
||||
cargo run -p libsignal-node-native_ts
|
||||
|
||||
alias generate-java := generate-jni
|
||||
alias generate-swift := generate-ffi
|
||||
|
||||
@ -24,8 +24,8 @@
|
||||
"build": "python3 build_node_bridge.py",
|
||||
"build-with-debug-level-logs": "python3 build_node_bridge.py --debug-level-logs",
|
||||
"clean": "rimraf dist build prebuilds",
|
||||
"format": "p() { prettier ${@:- --write} '**/*.{css,js,json,md,scss,ts,tsx}' ../rust/bridge/node/bin/Native.ts.in; }; p",
|
||||
"format-check": "p() { prettier ${@:- --check} '**/*.{css,js,json,md,scss,ts,tsx}' ../rust/bridge/node/bin/Native.ts.in; }; p",
|
||||
"format": "p() { prettier ${@:- --write} '**/*.{css,js,json,md,scss,ts,tsx}'; }; p",
|
||||
"format-check": "p() { prettier ${@:- --check} '**/*.{css,js,json,md,scss,ts,tsx}'; }; p",
|
||||
"install": "echo Use \\`npm run build\\` to build the native library if needed",
|
||||
"lint": "eslint .",
|
||||
"prepack": "cp ../acknowledgments/acknowledgments-desktop.md dist/acknowledgments.md",
|
||||
|
||||
3425
node/ts/Native.ts
3425
node/ts/Native.ts
File diff suppressed because it is too large
Load Diff
@ -80,8 +80,8 @@ class definition. For Swift, the `cbindgen` output is saved directly to a
|
||||
C-style `.h` file that the Swift toolchain can consume.
|
||||
|
||||
For TypeScript, the [`libsignal-node`] crate is expanded and processed by
|
||||
[`gen_ts_decl.py`](./node/bin/gen_ts_decl.py) and the output is interpolated into
|
||||
[`Native.ts.in`](./node/bin/Native.ts.in). The output, however, only
|
||||
[`libsignal-node-native_ts`](./node/native_ts/src/main.rs) and the output is interpolated into
|
||||
[`Native.ts.in`](./node/native_ts/src/Native.ts.in). The output, however, only
|
||||
declares the function signatures; to make them accessible to the JavaScript
|
||||
runtime, additional machinery is used. This takes the form of `#[linkme]`
|
||||
annotations on to the generated entry points; the [`linkme`] crate is used to
|
||||
|
||||
@ -15,17 +15,23 @@ workspace = true
|
||||
|
||||
[lib]
|
||||
name = "signal_node"
|
||||
crate-type = ["cdylib"]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
||||
[features]
|
||||
# Here for bridge_fn uniformity
|
||||
node = []
|
||||
default = ["node"]
|
||||
metadata = [
|
||||
"libsignal-bridge/metadata",
|
||||
"libsignal-bridge-testing/metadata",
|
||||
"libsignal-bridge-types/metadata",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
libsignal-bridge = { workspace = true, features = ["node", "signal-media"] }
|
||||
libsignal-bridge-macros = { workspace = true }
|
||||
libsignal-bridge-testing = { workspace = true, features = ["node", "signal-media"] }
|
||||
libsignal-bridge-types = { workspace = true, features = ["node"] }
|
||||
libsignal-protocol = { workspace = true }
|
||||
|
||||
futures = { workspace = true }
|
||||
|
||||
@ -1,358 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
#
|
||||
# Copyright (C) 2020-2021 Signal Messenger, LLC.
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
#
|
||||
|
||||
import collections
|
||||
import difflib
|
||||
import itertools
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Iterable, Iterator, Tuple
|
||||
|
||||
Args = collections.namedtuple('Args', ['verify'])
|
||||
|
||||
|
||||
def parse_args() -> Args:
|
||||
def print_usage_and_exit() -> None:
|
||||
print('usage: %s [--verify]' % sys.argv[0], file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
# If the command-line handling below gets any more complicated, this should be switched to argparse.
|
||||
mode = None
|
||||
if len(sys.argv) > 2:
|
||||
print_usage_and_exit()
|
||||
elif len(sys.argv) == 2:
|
||||
mode = sys.argv[1]
|
||||
if mode != '--verify':
|
||||
print_usage_and_exit()
|
||||
|
||||
return Args(verify=mode is not None)
|
||||
|
||||
|
||||
def split_rust_args(args: str) -> Iterator[Tuple[str, str]]:
|
||||
"""
|
||||
Split Rust `arg: Type` pairs separated by commas.
|
||||
|
||||
Account for templates, tuples, and slices.
|
||||
"""
|
||||
while ': ' in args:
|
||||
(name, args) = args.split(': ', maxsplit=1)
|
||||
if name.startswith('mut '):
|
||||
name = name[4:]
|
||||
open_pairs = 0
|
||||
for (i, c) in enumerate(args):
|
||||
if c == ',' and open_pairs == 0:
|
||||
ty = args[:i]
|
||||
args = args[i + 1:]
|
||||
yield (name.strip(), ty.strip())
|
||||
break
|
||||
elif c in ['<', '(', '[']:
|
||||
open_pairs += 1
|
||||
elif c in ['>', ')', ']']:
|
||||
open_pairs -= 1
|
||||
else:
|
||||
yield (name.strip(), args.strip())
|
||||
|
||||
|
||||
def translate_to_ts(typ: str) -> str:
|
||||
typ = typ.replace(' ', '')
|
||||
|
||||
type_map = {
|
||||
'()': 'void',
|
||||
'&[u8]': 'Uint8Array<ArrayBuffer>',
|
||||
'i32': 'number',
|
||||
'u8': 'number',
|
||||
'u16': 'number',
|
||||
'u32': 'number',
|
||||
'u64': 'bigint',
|
||||
'f64': 'number',
|
||||
'bool': 'boolean',
|
||||
'String': 'string',
|
||||
'&str': 'string',
|
||||
'Vec<u8>': 'Uint8Array<ArrayBuffer>',
|
||||
'Box<[u8]>': 'Uint8Array<ArrayBuffer>',
|
||||
'Box<[u32]>': 'Uint32Array<ArrayBuffer>',
|
||||
'bytes::Bytes': 'Uint8Array<ArrayBuffer>',
|
||||
'ServiceId': 'Uint8Array<ArrayBuffer>',
|
||||
'Aci': 'Uint8Array<ArrayBuffer>',
|
||||
'Pni': 'Uint8Array<ArrayBuffer>',
|
||||
'E164': 'string',
|
||||
"ServiceIdSequence<'_>": 'Uint8Array<ArrayBuffer>',
|
||||
'PathAndQuery': 'string',
|
||||
'LanguageList': 'string[]',
|
||||
'GroupSendFullToken': 'Uint8Array<ArrayBuffer>',
|
||||
'DeviceSpecifier': 'number',
|
||||
'&BackupKey': 'Uint8Array<ArrayBuffer>',
|
||||
'MultiRecipientSendAuthorization': 'Uint8Array<ArrayBuffer> | null',
|
||||
'DisconnectCause': 'Error | null',
|
||||
'::zkgroup::backups::BackupAuthCredential': 'Uint8Array<ArrayBuffer>',
|
||||
'::zkgroup::generic_server_params::GenericServerPublicParams': 'Uint8Array<ArrayBuffer>',
|
||||
}
|
||||
|
||||
if typ in type_map:
|
||||
return type_map[typ]
|
||||
|
||||
if typ.startswith('[u8;') or typ.startswith('&[u8;'):
|
||||
return 'Uint8Array<ArrayBuffer>'
|
||||
|
||||
if typ.startswith('&mutdyn'):
|
||||
return typ[7:]
|
||||
|
||||
if typ.startswith('&dyn'):
|
||||
return typ[4:]
|
||||
|
||||
if typ.startswith('&mut'):
|
||||
return 'Wrapper<' + typ[4:] + '>'
|
||||
|
||||
if typ.startswith('&[&'):
|
||||
assert typ.endswith(']')
|
||||
return 'Wrapper<' + translate_to_ts(typ[3:-1]) + '>[]'
|
||||
|
||||
if typ.startswith('Box<['):
|
||||
assert typ.endswith(']>')
|
||||
return translate_to_ts(typ[5:-2]) + '[]'
|
||||
|
||||
if typ.startswith('Box<dyn'):
|
||||
assert typ.endswith('>')
|
||||
return translate_to_ts(typ[7:-1])
|
||||
|
||||
if typ.startswith('Vec<'):
|
||||
assert typ.endswith('>')
|
||||
return translate_to_ts(typ[4:-1]) + '[]'
|
||||
|
||||
if typ.startswith('&['):
|
||||
assert typ.endswith(']')
|
||||
return 'Wrapper<' + translate_to_ts(typ[2:-1]) + '>[]'
|
||||
|
||||
if typ.startswith('&'):
|
||||
return 'Wrapper<' + typ[1:] + '>'
|
||||
|
||||
if typ.startswith('('):
|
||||
assert typ.endswith(')'), typ
|
||||
inner = typ[1:-1].split(',')
|
||||
if len(inner) == 1:
|
||||
return translate_to_ts(inner[0])
|
||||
return '[' + ', '.join(translate_to_ts(x) for x in inner) + ']'
|
||||
|
||||
if typ.startswith('Option<'):
|
||||
assert typ.endswith('>')
|
||||
return translate_to_ts(typ[7:-1]) + ' | null'
|
||||
|
||||
if typ.startswith('Result<'):
|
||||
assert typ.endswith('>')
|
||||
type_args = typ[7:-1]
|
||||
(success_type, *failure_type) = type_args.rsplit(',', 1)
|
||||
if failure_type and ')' in failure_type[0]:
|
||||
success_type = type_args
|
||||
return translate_to_ts(success_type)
|
||||
|
||||
if typ.startswith('std::result::Result<'):
|
||||
assert typ.endswith('>')
|
||||
type_args = typ[20:-1]
|
||||
(success_type, *failure_type) = type_args.rsplit(',', 1)
|
||||
if failure_type and ')' in failure_type[0]:
|
||||
success_type = type_args
|
||||
return translate_to_ts(success_type)
|
||||
|
||||
if typ.startswith('Promise<'):
|
||||
assert typ.endswith('>')
|
||||
return 'Promise<' + translate_to_ts(typ[8:-1]) + '>'
|
||||
|
||||
if typ.startswith('CancellablePromise<'):
|
||||
assert typ.endswith('>')
|
||||
return 'CancellablePromise<' + translate_to_ts(typ[19:-1]) + '>'
|
||||
|
||||
if typ.startswith('AsType<'):
|
||||
assert typ.endswith('>')
|
||||
assert ',' in typ
|
||||
return translate_to_ts(typ.split(',')[1][:-1])
|
||||
|
||||
if typ.startswith('Ignored<'):
|
||||
assert typ.endswith('>')
|
||||
return 'null'
|
||||
|
||||
return typ
|
||||
|
||||
|
||||
DIAGNOSTICS_TO_IGNORE = [
|
||||
r'warning: \d+ warnings? emitted',
|
||||
r'warning: unused import',
|
||||
r'warning: field.+ never read',
|
||||
r'warning: variant.+ never constructed',
|
||||
r'warning: method.+ never used',
|
||||
r'warning: associated function.+ never used',
|
||||
]
|
||||
SHOULD_IGNORE_PATTERN = re.compile('(' + ')|('.join(DIAGNOSTICS_TO_IGNORE) + ')')
|
||||
|
||||
|
||||
def camelcase(arg: str) -> str:
|
||||
return re.sub(
|
||||
# Preserve double-underscores and leading underscores,
|
||||
# but remove single underscores and capitalize the following letter.
|
||||
r'([^_])_([^_])',
|
||||
lambda match: match.group(1) + match.group(2).upper(),
|
||||
arg)
|
||||
|
||||
|
||||
def rewrite_function_as_property(ts_function: str) -> str:
|
||||
return ts_function.replace('(', ': (', 1).replace('):', ') =>')
|
||||
|
||||
|
||||
def rewrite_fn(function_match: re.Match[str]) -> str:
|
||||
(prefix, fn_args, ret_type) = function_match.groups()
|
||||
|
||||
ts_ret_type = translate_to_ts(ret_type)
|
||||
ts_args = []
|
||||
|
||||
for (arg_name, arg_type) in split_rust_args(fn_args):
|
||||
ts_arg_type = translate_to_ts(arg_type)
|
||||
ts_args.append('%s: %s' % (camelcase(arg_name.strip()), ts_arg_type))
|
||||
|
||||
return '%s(%s): %s;' % (prefix, ', '.join(ts_args), ts_ret_type)
|
||||
|
||||
|
||||
def rewrite_trait(decl: str, function_sig: re.Pattern[str]) -> Iterator[str]:
|
||||
for line in decl.split('\\n'):
|
||||
if function_match := function_sig.match(line.rstrip(';')):
|
||||
yield ' ' + rewrite_function_as_property(rewrite_fn(function_match))
|
||||
continue
|
||||
|
||||
# Fix backslash-escaped double-quotes.
|
||||
yield bytes(line, 'utf-8').decode('unicode_escape')
|
||||
|
||||
|
||||
def collect_decls(crate_dir: str, features: Iterable[str] = ()) -> Iterator[str]:
|
||||
args = [
|
||||
'cargo',
|
||||
'rustc',
|
||||
'-q',
|
||||
'--profile=check',
|
||||
'--features', ','.join(features),
|
||||
'--message-format=short',
|
||||
'--color=never',
|
||||
'--',
|
||||
'-Zunpretty=expanded']
|
||||
rustc = subprocess.Popen(args, cwd=crate_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
(raw_stdout, raw_stderr) = rustc.communicate()
|
||||
|
||||
stdout = str(raw_stdout.decode('utf8'))
|
||||
stderr = str(raw_stderr.decode('utf8'))
|
||||
|
||||
had_error = False
|
||||
for l in stderr.split('\n'):
|
||||
if l == '':
|
||||
continue
|
||||
|
||||
if SHOULD_IGNORE_PATTERN.search(l):
|
||||
continue
|
||||
|
||||
print(l, file=sys.stderr)
|
||||
had_error = True
|
||||
|
||||
if had_error:
|
||||
print('Exiting with error')
|
||||
sys.exit(1)
|
||||
|
||||
comment_decl = re.compile(r'\s*///\s*ts: `(.+)`')
|
||||
# Note that the doc attribute is sometimes wrapped onto two lines.
|
||||
attr_decl = re.compile(r'\s*(?:#\[doc\s*=\s*)?"ts: `(.+)`"\]')
|
||||
|
||||
# Make sure /not/ to match arguments with nested parentheses,
|
||||
# which won't survive textual splitting below.
|
||||
function_sig = re.compile(r'(.+)\(([^()]*)\): (.+)')
|
||||
|
||||
for line in stdout.split('\n'):
|
||||
match = comment_decl.match(line) or attr_decl.match(line)
|
||||
if match is None:
|
||||
continue
|
||||
|
||||
(decl,) = match.groups()
|
||||
|
||||
if decl.startswith('export /*trait*/ type '):
|
||||
yield '\n'.join(rewrite_trait(decl, function_sig))
|
||||
continue
|
||||
|
||||
if function_match := function_sig.match(decl):
|
||||
yield rewrite_fn(function_match)
|
||||
continue
|
||||
|
||||
# Fix backslash-escaped double-quotes.
|
||||
yield bytes(decl, 'utf-8').decode('unicode_escape')
|
||||
|
||||
|
||||
def expand_template(template_file: str, decls: Iterable[str]) -> str:
|
||||
decls = list(decls)
|
||||
with open(template_file, 'r') as f:
|
||||
contents = f.read()
|
||||
|
||||
# Rewrite from function syntax to property syntax to take advantage of
|
||||
# https://www.typescriptlang.org/tsconfig/#strictFunctionTypes.
|
||||
contents = contents.replace('NATIVE_FNS;', '\n '.join(
|
||||
rewrite_function_as_property(x.removeprefix('export function '))
|
||||
for x in decls if x.startswith('export function ')
|
||||
))
|
||||
contents = contents.replace('NATIVE_FN_NAMES', ''.join(
|
||||
'\n ' + x.removeprefix('export function ').split('(')[0] + ','
|
||||
for x in decls if x.startswith('export function ')
|
||||
) + '\n')
|
||||
contents = contents.replace('NATIVE_TYPES;', '\n'.join(
|
||||
'export ' + x.removeprefix('export ') for x in decls if not x.startswith('export function ')
|
||||
))
|
||||
|
||||
return contents
|
||||
|
||||
|
||||
def verify_contents(expected_output_file: str, expected_contents: str) -> None:
|
||||
with open(expected_output_file) as fh:
|
||||
current_contents = fh.readlines()
|
||||
diff = difflib.unified_diff(current_contents, expected_contents.splitlines(keepends=True))
|
||||
first_line = next(diff, None)
|
||||
if first_line:
|
||||
sys.stdout.write(first_line)
|
||||
sys.stdout.writelines(diff)
|
||||
sys.exit(f'error: {expected_output_file} not up to date; re-run {sys.argv[0]}!')
|
||||
|
||||
|
||||
Crate = collections.namedtuple('Crate', ['path', 'features'], defaults=[()])
|
||||
|
||||
|
||||
def convert_to_typescript(rust_crates: Iterable[Crate], ts_in_path: str, ts_out_path: str, verify: bool) -> None:
|
||||
decls = itertools.chain.from_iterable(collect_decls(crate.path, crate.features) for crate in rust_crates)
|
||||
contents = expand_template(ts_in_path, decls)
|
||||
|
||||
if not os.access(ts_out_path, os.F_OK):
|
||||
raise Exception(f"Didn't find {ts_out_path} where it was expected")
|
||||
|
||||
if not verify:
|
||||
with open(ts_out_path, 'w') as fh:
|
||||
fh.write(contents)
|
||||
else:
|
||||
verify_contents(ts_out_path, contents)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
our_abs_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
output_file_name = 'Native.ts'
|
||||
|
||||
convert_to_typescript(
|
||||
rust_crates=[
|
||||
Crate(path=os.path.join(our_abs_dir, '..')),
|
||||
Crate(path=os.path.join(our_abs_dir, '..', '..', 'shared'), features=('node', 'signal-media')),
|
||||
Crate(path=os.path.join(our_abs_dir, '..', '..', 'shared', 'types'), features=('node', 'signal-media')),
|
||||
Crate(path=os.path.join(our_abs_dir, '..', '..', 'shared', 'testing'), features=('node', 'signal-media')),
|
||||
],
|
||||
ts_in_path=os.path.join(our_abs_dir, output_file_name + '.in'),
|
||||
ts_out_path=os.path.join(our_abs_dir, '..', '..', '..', '..', 'node', 'ts', output_file_name),
|
||||
verify=args.verify,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
19
rust/bridge/node/native_ts/Cargo.toml
Normal file
19
rust/bridge/node/native_ts/Cargo.toml
Normal file
@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "libsignal-node-native_ts"
|
||||
version = "0.1.0"
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
edition = "2024"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
libsignal-bridge = { workspace = true, features = ["node", "metadata"] }
|
||||
libsignal-bridge-testing = { workspace = true, features = ["node", "metadata"] }
|
||||
libsignal-bridge-types = { workspace = true, features = ["node", "metadata"] }
|
||||
libsignal-node = { workspace = true, features = ["node", "metadata"] }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
minijinja = { workspace = true, features = ["preserve_order"] }
|
||||
@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright 2020 Signal Messenger, LLC.
|
||||
// Copyright 2026 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
@ -134,18 +134,60 @@ export type Serialized<T> = Uint8Array<ArrayBuffer>;
|
||||
type ConnectChatBridge = Wrapper<ConnectionManager>;
|
||||
type TestingFutureCancellationGuard = Wrapper<TestingFutureCancellationCounter>;
|
||||
|
||||
// Keep in sync with rust/bridge/node/src/logging.rs
|
||||
export const enum LogLevel { Error = 1, Warn, Info, Debug, Trace }
|
||||
|
||||
/* eslint-disable comma-dangle */
|
||||
export const NetRemoteConfigKeys = [
|
||||
{%- for key in remote_config_keys -%}
|
||||
'{{ key }}',
|
||||
{%- endfor -%}
|
||||
] as const;
|
||||
|
||||
import load from 'node-gyp-build';
|
||||
|
||||
type NativeFunctions = {
|
||||
registerErrors: (errorsModule: Record<string, unknown>) => void;
|
||||
NATIVE_FNS;
|
||||
initLogger: (maxLevel: LogLevel, callback: (level: LogLevel, target: string, file: string | null, line: number | null, message: string) => void) => void;
|
||||
{%- for (name, f) in ctx.native_functions|items %}
|
||||
{{ name }}: (
|
||||
{%- for (name, ty) in f.arguments -%}
|
||||
{{ name }}: {{ ty }},
|
||||
{%- endfor -%}
|
||||
) => {{ f.return_type }};
|
||||
{%- endfor %}
|
||||
};
|
||||
|
||||
const { registerErrors, NATIVE_FN_NAMES } = load(
|
||||
{% macro native_fn_names(ctx) %}
|
||||
{%- for (name, f) in ctx.native_functions|items %}
|
||||
{{ name }},
|
||||
{%- endfor %}
|
||||
{% endmacro %}
|
||||
|
||||
const { registerErrors,
|
||||
initLogger,
|
||||
{{ native_fn_names(ctx) }}
|
||||
} = load(
|
||||
`${import.meta.dirname}/../`
|
||||
) as NativeFunctions;
|
||||
|
||||
export { registerErrors, NATIVE_FN_NAMES };
|
||||
export { registerErrors,
|
||||
initLogger,
|
||||
{{ native_fn_names(ctx)
|
||||
}} };
|
||||
|
||||
/* eslint-disable comma-dangle */
|
||||
NATIVE_TYPES;
|
||||
{% for (name, fns) in ctx.bridge_traits|items %}
|
||||
export /*trait*/ type {{ name }} = {
|
||||
{%- for fn in fns %}
|
||||
{{ fn.name }}: (
|
||||
{%- for (arg, ty) in fn.body.arguments -%}
|
||||
{{ arg }}: {{ ty }},
|
||||
{%- endfor -%}
|
||||
) => {{ fn.body.return_type }};
|
||||
{%- endfor %}
|
||||
};
|
||||
{% endfor %}
|
||||
|
||||
{% for ty in ctx.opaque_types -%}
|
||||
export interface {{ ty }} { readonly __type: unique symbol; }
|
||||
{% endfor -%}
|
||||
51
rust/bridge/node/native_ts/src/main.rs
Normal file
51
rust/bridge/node/native_ts/src/main.rs
Normal file
@ -0,0 +1,51 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// To make sure the linkmes work
|
||||
extern crate libsignal_bridge;
|
||||
extern crate libsignal_bridge_testing;
|
||||
extern crate signal_node;
|
||||
|
||||
use clap::Parser;
|
||||
use libsignal_bridge_types::metadata::node::TsMetadataContext;
|
||||
use libsignal_bridge_types::net::remote_config::RemoteConfigKey;
|
||||
use minijinja::context;
|
||||
|
||||
#[derive(Parser)]
|
||||
/// Regenerate Native.ts
|
||||
///
|
||||
/// This command assumes it's being invoked from the workspace root.
|
||||
struct Cli {
|
||||
/// Don't actually overwrite Native.ts, just make sure it's up-to-date.
|
||||
#[clap(long)]
|
||||
verify: bool,
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let args = Cli::parse();
|
||||
let mut env = minijinja::Environment::new();
|
||||
env.set_undefined_behavior(minijinja::UndefinedBehavior::Strict);
|
||||
env.add_template("Native.ts.in", include_str!("Native.ts.in"))?;
|
||||
let tmpl = env.get_template("Native.ts.in")?;
|
||||
let mut ctx = TsMetadataContext::default();
|
||||
for item in libsignal_bridge_types::metadata::node::NODE_ITEMS.iter() {
|
||||
// We don't check item.module_path because, unlike other client languages, we emit both
|
||||
// testing and non-testing native into the same typescript file.
|
||||
(item.apply)(&mut ctx);
|
||||
}
|
||||
let code = tmpl.render(context! {
|
||||
ctx => ctx,
|
||||
remote_config_keys => RemoteConfigKey::KEYS,
|
||||
})?;
|
||||
let dst = "./node/ts/Native.ts";
|
||||
if args.verify {
|
||||
anyhow::ensure!(
|
||||
std::fs::read_to_string(dst)? == code,
|
||||
"Native.ts is not up-to-date"
|
||||
);
|
||||
} else {
|
||||
std::fs::write(dst, code.as_bytes())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -6,7 +6,7 @@
|
||||
#![warn(clippy::unwrap_used)]
|
||||
|
||||
use futures::executor;
|
||||
use libsignal_bridge::node::{AssumedImmutableBuffer, ResultTypeInfo, SignalNodeError};
|
||||
use libsignal_bridge::node::ResultTypeInfo;
|
||||
use libsignal_bridge::node_register;
|
||||
use libsignal_bridge::support::*;
|
||||
use libsignal_bridge_macros::bridge_fn;
|
||||
@ -16,7 +16,6 @@ use minidump_processor::ProcessorOptions;
|
||||
use minidump_unwind::Symbolizer;
|
||||
use minidump_unwind::symbols::string_symbol_supplier;
|
||||
use neon::prelude::*;
|
||||
use neon::types::buffer::TypedArray;
|
||||
use rand::TryRngCore;
|
||||
use uuid::Uuid;
|
||||
|
||||
@ -31,11 +30,6 @@ use libsignal_bridge_testing::*;
|
||||
fn main(mut cx: ModuleContext) -> NeonResult<()> {
|
||||
libsignal_bridge::node::register(&mut cx)?;
|
||||
cx.export_function("initLogger", logging::init_logger)?;
|
||||
cx.export_function(
|
||||
"SealedSenderMultiRecipientMessage_Parse",
|
||||
sealed_sender_multi_recipient_message_parse,
|
||||
)?;
|
||||
cx.export_function("MinidumpToJSONString", minidump_to_json_string)?;
|
||||
let remote_config_keys = libsignal_bridge::net::RemoteConfigKey::KEYS.convert_into(&mut cx)?;
|
||||
cx.export_value("NetRemoteConfigKeys", remote_config_keys)?;
|
||||
Ok(())
|
||||
@ -67,91 +61,96 @@ impl<'a> From<ArrayBuilder<'a>> for Handle<'a, JsArray> {
|
||||
}
|
||||
}
|
||||
|
||||
/// ts: `export function SealedSenderMultiRecipientMessage_Parse(buffer: Uint8Array<ArrayBuffer>): SealedSenderMultiRecipientMessage`
|
||||
fn sealed_sender_multi_recipient_message_parse(mut cx: FunctionContext) -> JsResult<JsObject> {
|
||||
let buffer_arg = cx.argument::<JsUint8Array>(0)?;
|
||||
let buffer = AssumedImmutableBuffer::new(&cx, buffer_arg);
|
||||
let messages = match SealedSenderV2SentMessage::parse(&buffer) {
|
||||
Ok(messages) => messages,
|
||||
Err(e) => {
|
||||
let throwable =
|
||||
e.into_throwable(&mut cx, "sealed_sender_multi_recipient_parse_sent_message");
|
||||
cx.throw(throwable)?
|
||||
}
|
||||
};
|
||||
struct SealedSenderMultiRecipientMessage<'a>(SealedSenderV2SentMessage<'a>);
|
||||
impl<'a, 'b> ResultTypeInfo<'a> for SealedSenderMultiRecipientMessage<'b> {
|
||||
type ResultType = JsObject;
|
||||
|
||||
let recipient_map = cx.empty_object();
|
||||
let mut excluded_recipients_array = ArrayBuilder::new(&mut cx);
|
||||
fn convert_into(self, cx: &mut impl Context<'a>) -> JsResult<'a, Self::ResultType> {
|
||||
let messages = self.0;
|
||||
let recipient_map = cx.empty_object();
|
||||
let mut excluded_recipients_array = ArrayBuilder::new(cx);
|
||||
|
||||
for (service_id, recipient) in &messages.recipients {
|
||||
let service_id_string = cx.string(service_id.service_id_string());
|
||||
if recipient.devices.is_empty() {
|
||||
excluded_recipients_array
|
||||
.push(service_id_string, &mut cx)
|
||||
.expect("failed to construct output array");
|
||||
continue;
|
||||
for (service_id, recipient) in &messages.recipients {
|
||||
let service_id_string = cx.string(service_id.service_id_string());
|
||||
if recipient.devices.is_empty() {
|
||||
excluded_recipients_array
|
||||
.push(service_id_string, cx)
|
||||
.expect("failed to construct output array");
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut device_ids = ArrayBuilder::new(cx);
|
||||
let mut registration_ids = ArrayBuilder::new(cx);
|
||||
|
||||
for &(device_id, registration_id) in &recipient.devices {
|
||||
device_ids
|
||||
.push(cx.number(u32::from(device_id)), cx)
|
||||
.expect("failed to construct output array");
|
||||
registration_ids
|
||||
.push(cx.number(registration_id), cx)
|
||||
.expect("failed to construct output array");
|
||||
}
|
||||
|
||||
let range = messages.range_for_recipient_key_material(recipient);
|
||||
let range_start = cx.number(u32::try_from(range.start).expect("message too large"));
|
||||
let range_len = cx.number(u32::try_from(range.len()).expect("message too large"));
|
||||
|
||||
let recipient_object = cx.empty_object();
|
||||
recipient_object
|
||||
.set(cx, "deviceIds", device_ids.into())
|
||||
.expect("failed to construct recipient object");
|
||||
recipient_object
|
||||
.set(cx, "registrationIds", registration_ids.into())
|
||||
.expect("failed to construct recipient object");
|
||||
recipient_object
|
||||
.set(cx, "rangeOffset", range_start)
|
||||
.expect("failed to construct recipient object");
|
||||
recipient_object
|
||||
.set(cx, "rangeLen", range_len)
|
||||
.expect("failed to construct recipient object");
|
||||
|
||||
recipient_map
|
||||
.set(cx, service_id_string, recipient_object)
|
||||
.expect("failed to record recipient object");
|
||||
}
|
||||
|
||||
let mut device_ids = ArrayBuilder::new(&mut cx);
|
||||
let mut registration_ids = ArrayBuilder::new(&mut cx);
|
||||
let offset_of_shared_bytes =
|
||||
cx.number(u32::try_from(messages.offset_of_shared_bytes()).expect("message too large"));
|
||||
|
||||
for &(device_id, registration_id) in &recipient.devices {
|
||||
device_ids
|
||||
.push(cx.number(u32::from(device_id)), &mut cx)
|
||||
.expect("failed to construct output array");
|
||||
registration_ids
|
||||
.push(cx.number(registration_id), &mut cx)
|
||||
.expect("failed to construct output array");
|
||||
}
|
||||
let result = cx.empty_object();
|
||||
result
|
||||
.set(cx, "recipientMap", recipient_map)
|
||||
.expect("failed to construct result object");
|
||||
result
|
||||
.set(cx, "excludedRecipients", excluded_recipients_array.into())
|
||||
.expect("failed to construct result object");
|
||||
result
|
||||
.set(cx, "offsetOfSharedData", offset_of_shared_bytes)
|
||||
.expect("failed to construct result object");
|
||||
|
||||
let range = messages.range_for_recipient_key_material(recipient);
|
||||
let range_start = cx.number(u32::try_from(range.start).expect("message too large"));
|
||||
let range_len = cx.number(u32::try_from(range.len()).expect("message too large"));
|
||||
|
||||
let recipient_object = cx.empty_object();
|
||||
recipient_object
|
||||
.set(&mut cx, "deviceIds", device_ids.into())
|
||||
.expect("failed to construct recipient object");
|
||||
recipient_object
|
||||
.set(&mut cx, "registrationIds", registration_ids.into())
|
||||
.expect("failed to construct recipient object");
|
||||
recipient_object
|
||||
.set(&mut cx, "rangeOffset", range_start)
|
||||
.expect("failed to construct recipient object");
|
||||
recipient_object
|
||||
.set(&mut cx, "rangeLen", range_len)
|
||||
.expect("failed to construct recipient object");
|
||||
|
||||
recipient_map
|
||||
.set(&mut cx, service_id_string, recipient_object)
|
||||
.expect("failed to record recipient object");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
let offset_of_shared_bytes =
|
||||
cx.number(u32::try_from(messages.offset_of_shared_bytes()).expect("message too large"));
|
||||
|
||||
let result = cx.empty_object();
|
||||
result
|
||||
.set(&mut cx, "recipientMap", recipient_map)
|
||||
.expect("failed to construct result object");
|
||||
result
|
||||
.set(
|
||||
&mut cx,
|
||||
"excludedRecipients",
|
||||
excluded_recipients_array.into(),
|
||||
)
|
||||
.expect("failed to construct result object");
|
||||
result
|
||||
.set(&mut cx, "offsetOfSharedData", offset_of_shared_bytes)
|
||||
.expect("failed to construct result object");
|
||||
|
||||
Ok(result)
|
||||
#[cfg(feature = "metadata")]
|
||||
fn register_ts_ffi_type(
|
||||
_: &mut libsignal_bridge_types::metadata::node::TsMetadataContext,
|
||||
) -> String {
|
||||
"SealedSenderMultiRecipientMessage".into()
|
||||
}
|
||||
}
|
||||
|
||||
/// ts: `export function MinidumpToJSONString(buffer: Uint8Array<ArrayBuffer>): string`
|
||||
fn minidump_to_json_string(mut cx: FunctionContext) -> JsResult<JsString> {
|
||||
let buffer_arg = cx.argument::<JsUint8Array>(0)?;
|
||||
let dump = Minidump::read(buffer_arg.as_slice(&cx)).expect("Failed to parse minidump");
|
||||
#[bridge_fn(jni = false, ffi = false)]
|
||||
fn SealedSenderMultiRecipientMessage_Parse(
|
||||
buffer: &[u8],
|
||||
) -> libsignal_protocol::error::Result<SealedSenderMultiRecipientMessage<'_>> {
|
||||
Ok(SealedSenderMultiRecipientMessage(
|
||||
SealedSenderV2SentMessage::parse(buffer)?,
|
||||
))
|
||||
}
|
||||
|
||||
#[bridge_fn(ffi = false, jni = false)]
|
||||
fn MinidumpToJSONString(buffer: &[u8]) -> String {
|
||||
let dump = Minidump::read(buffer).expect("Failed to parse minidump");
|
||||
let provider = Symbolizer::new(string_symbol_supplier(std::collections::HashMap::new()));
|
||||
let options = ProcessorOptions::default();
|
||||
|
||||
@ -165,7 +164,7 @@ fn minidump_to_json_string(mut cx: FunctionContext) -> JsResult<JsString> {
|
||||
.print_json(&mut json, false)
|
||||
.expect("Failed to print json");
|
||||
|
||||
Ok(cx.string(std::str::from_utf8(&json).expect("Failed to convert JSON to utf8")))
|
||||
String::from_utf8(json).expect("Failed to convert JSON to utf8")
|
||||
}
|
||||
|
||||
#[bridge_fn(ffi = false, jni = false)]
|
||||
|
||||
@ -9,7 +9,7 @@ use std::sync::atomic::AtomicBool;
|
||||
use libsignal_bridge::node::SimpleArgTypeInfo;
|
||||
use neon::prelude::*;
|
||||
|
||||
/// ts: `export const enum LogLevel { Error = 1, Warn, Info, Debug, Trace }`
|
||||
// Keep in sync with Native.ts.in
|
||||
#[derive(Clone, Copy)]
|
||||
enum LogLevel {
|
||||
Error = 1,
|
||||
@ -194,7 +194,6 @@ fn set_max_level_from_js_level(max_level: u32) {
|
||||
log::set_max_level(log::Level::from(level).to_level_filter());
|
||||
}
|
||||
|
||||
/// ts: `export function initLogger(maxLevel: LogLevel, callback: (level: LogLevel, target: string, file: string | null, line: number | null, message: string) => void): void`
|
||||
pub(crate) fn init_logger(mut cx: FunctionContext) -> JsResult<JsUndefined> {
|
||||
let max_level_arg = cx.argument::<JsNumber>(0)?;
|
||||
let max_level = u32::convert_from(&mut cx, max_level_arg)?;
|
||||
|
||||
@ -71,3 +71,4 @@ ffi = ["libsignal-bridge-types/ffi"]
|
||||
jni = ["dep:jni", "libsignal-bridge-types/jni"]
|
||||
node = ["neon", "linkme", "libsignal-bridge-types/node"]
|
||||
signal-media = ["dep:signal-media", "libsignal-bridge-types/signal-media"]
|
||||
metadata = ["libsignal-bridge-types/metadata"]
|
||||
|
||||
@ -52,7 +52,7 @@
|
||||
//! export function SenderKeyMessage_New(
|
||||
//! keyId: number,
|
||||
//! iteration: number,
|
||||
//! ciphertext: Buffer,
|
||||
//! ciphertext: Uint8Array<ArrayBuffer>,
|
||||
//! pk: Wrapper<PrivateKey>
|
||||
//! ): SenderKeyMessage;
|
||||
//! ```
|
||||
@ -132,8 +132,9 @@
|
||||
//!
|
||||
//! 1. Argument and result types for FFI and JNI are determined by macros `ffi_arg_type`,
|
||||
//! `ffi_result_type`, `jni_arg_type`, and `jni_result_type`. You may need to add your new type
|
||||
//! there. JNI and Node types also undergo some additional transformation in the scripts
|
||||
//! `gen_java_decl.py` and `gen_ts_decl.py`, which you may need to tweak as well.
|
||||
//! there. JNI types also undergo some additional transformation in the scripts
|
||||
//! `gen_java_decl.py`, which you may need to tweak as well. Node types are generated as Strings
|
||||
//! via the `gen_ts_ffi()` methods on `node::{AsyncArg, Arg, Result}TypeInfo`.
|
||||
//!
|
||||
//! 2. Argument types conform to one or more of the following bridge-specific traits:
|
||||
//!
|
||||
|
||||
@ -12,7 +12,7 @@ use syn::*;
|
||||
use syn_mid::Signature;
|
||||
|
||||
use crate::BridgingKind;
|
||||
use crate::util::{extract_arg_names_and_types, result_type};
|
||||
use crate::util::{crates, extract_arg_names_and_types, result_type};
|
||||
|
||||
fn bridge_fn_body(orig_name: &Ident, input_args: &[(&Ident, &Type)]) -> TokenStream2 {
|
||||
// Scroll down to the end of the function to see the quote template.
|
||||
@ -182,9 +182,14 @@ pub(crate) fn bridge_fn(
|
||||
let name_with_prefix = format_ident!("node_{}", name);
|
||||
let name_without_prefix = Ident::new(name, Span::call_site());
|
||||
|
||||
let ts_signature_comment = generate_ts_signature_comment(name, sig, bridging_kind);
|
||||
|
||||
let input_args = extract_arg_names_and_types(sig)?;
|
||||
let ts_metadata = generate_ts_metadata(
|
||||
name,
|
||||
sig.asyncness.is_some(),
|
||||
&input_args,
|
||||
result_type(&sig.output),
|
||||
bridging_kind,
|
||||
);
|
||||
|
||||
let body = match (sig.asyncness, bridging_kind) {
|
||||
(Some(_), _) => bridge_fn_async_body(&sig.ident, name, bridging_kind, &input_args),
|
||||
@ -200,51 +205,83 @@ pub(crate) fn bridge_fn(
|
||||
Ok(quote! {
|
||||
#[cfg(feature = "node")]
|
||||
#[allow(non_snake_case)]
|
||||
#[doc = #ts_signature_comment]
|
||||
pub fn #name_with_prefix(
|
||||
mut cx: node::FunctionContext,
|
||||
) -> node::JsResult<node::JsValue> {
|
||||
#body
|
||||
}
|
||||
#[cfg(all(feature = "metadata", feature = "node"))]
|
||||
#ts_metadata
|
||||
|
||||
#[cfg(feature = "node")]
|
||||
node_register!(#name_without_prefix);
|
||||
})
|
||||
}
|
||||
|
||||
/// Generates a string, containing the *Rust* signature of a bridged function, that gen_ts_decl.py
|
||||
/// can use to generate Native.d.ts.
|
||||
fn generate_ts_signature_comment(
|
||||
/// Generates the code to embed `libsignal_bridge_types::metadata` metadata
|
||||
fn generate_ts_metadata(
|
||||
name_without_prefix: &str,
|
||||
sig: &Signature,
|
||||
asyncness: bool,
|
||||
input_args: &[(&Ident, &Type)],
|
||||
result_type: TokenStream2,
|
||||
bridging_kind: &BridgingKind,
|
||||
) -> String {
|
||||
let mut ts_args = vec![];
|
||||
) -> TokenStream2 {
|
||||
let krate = crates::libsignal_bridge_types();
|
||||
let mut input_args: Vec<_> = input_args
|
||||
.iter()
|
||||
.map(|(name, ty)| (name.to_string(), ty.to_token_stream()))
|
||||
.collect();
|
||||
match bridging_kind {
|
||||
BridgingKind::Regular => {}
|
||||
BridgingKind::Io { runtime } => {
|
||||
ts_args.push(format!("async_runtime: &{}", runtime.to_token_stream()))
|
||||
let runtime = runtime.to_token_stream();
|
||||
input_args.insert(0, ("async_runtime".to_string(), quote!(&#runtime)))
|
||||
}
|
||||
}
|
||||
ts_args.extend(
|
||||
sig.inputs
|
||||
.iter()
|
||||
.map(|arg| arg.to_token_stream().to_string().replace('\n', " ")),
|
||||
);
|
||||
|
||||
let result_type_format = match (sig.asyncness, bridging_kind) {
|
||||
(Some(_), BridgingKind::Io { .. }) => |ty| format!("CancellablePromise<{ty}>"),
|
||||
(Some(_), _) => |ty| format!("Promise<{ty}>"),
|
||||
(None, _) => |ty| format!("{ty}"),
|
||||
let argument_names = input_args
|
||||
.iter()
|
||||
.map(|(x, _)| to_lower_camel_case_preserve_underscores(x))
|
||||
.collect_vec();
|
||||
let argument_types = input_args.iter().map(|(_, x)| x).collect_vec();
|
||||
let return_type_format = match (asyncness, bridging_kind) {
|
||||
(true, BridgingKind::Io { .. }) => "CancellablePromise<{return_type}>",
|
||||
(true, _) => "Promise<{return_type}>",
|
||||
(false, _) => "{return_type}",
|
||||
};
|
||||
let result_type_str = result_type_format(result_type(&sig.output));
|
||||
let md = quote!(#krate::metadata);
|
||||
let metadata_name = format_ident!("_BRIDGE_NODE_METADATA_{name_without_prefix}");
|
||||
let type_info_trait = if asyncness {
|
||||
quote!(AsyncArgTypeInfo)
|
||||
} else {
|
||||
quote!(ArgTypeInfo)
|
||||
};
|
||||
quote! {
|
||||
#[#md::linkme::distributed_slice(#md::node::NODE_ITEMS)]
|
||||
#[linkme(crate = #md::linkme)]
|
||||
static #metadata_name: #md::FnWithModule<#md::node::TsMetadataContext> = #md::FnWithModule {
|
||||
module_path: module_path!(),
|
||||
apply: |ctx| {
|
||||
use #md::node::result_type_helper::*;
|
||||
let return_type: ResultMetadataTransformHelper<#result_type> = Default::default();
|
||||
let return_type = return_type.register_ts_ffi_type(ctx);
|
||||
let mut arguments = Vec::new();
|
||||
#(arguments.push((
|
||||
#argument_names.into(),
|
||||
<#argument_types as #krate::node::#type_info_trait>::register_ts_ffi_type(ctx)
|
||||
));)*
|
||||
ctx.native_functions.insert(
|
||||
#name_without_prefix.into(),
|
||||
#md::node::NativeFunction { arguments, return_type: format!(#return_type_format) },
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
format!(
|
||||
"ts: `export function {}({}): {}`",
|
||||
name_without_prefix,
|
||||
ts_args.join(", "),
|
||||
result_type_str
|
||||
)
|
||||
fn to_lower_camel_case_preserve_underscores(x: &str) -> String {
|
||||
let x_sans_underscore = x.trim_start_matches('_');
|
||||
let core = x_sans_underscore.to_lower_camel_case();
|
||||
format!("{}{core}", &x[0..(x.len() - x_sans_underscore.len())])
|
||||
}
|
||||
|
||||
pub(crate) fn name_from_ident(ident: &Ident) -> String {
|
||||
@ -259,23 +296,20 @@ pub(crate) fn name_from_ident(ident: &Ident) -> String {
|
||||
pub(crate) fn bridge_trait(trait_to_bridge: &ItemTrait, js_name: &str) -> Result<TokenStream2> {
|
||||
let trait_name = &trait_to_bridge.ident;
|
||||
let wrapper_name = format_ident!("Node{}", trait_to_bridge.ident);
|
||||
let krate = crates::libsignal_bridge_types();
|
||||
|
||||
let callbacks = trait_to_bridge
|
||||
.items
|
||||
.iter()
|
||||
.map(bridge_callback_item)
|
||||
.map(|x| bridge_callback_item(x, &krate))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let callback_impls = callbacks.iter().map(|c| &c.implementation);
|
||||
let callback_ts_decls = callbacks.iter().map(|c| &c.ts_decl);
|
||||
|
||||
let ts_declaration_comment = format!(
|
||||
"ts: `export /*trait*/ type {js_name} = {{\n{}\n}};`",
|
||||
callback_ts_decls.format("\n")
|
||||
);
|
||||
let callback_bridge_trait_functions = callbacks.iter().map(|c| &c.bridge_trait_function);
|
||||
let md = quote!(#krate::metadata);
|
||||
let metadata_name = format_ident!("_BRIDGE_NODE_METADATA_{trait_name}");
|
||||
|
||||
Ok(quote! {
|
||||
#[cfg(feature = "node")]
|
||||
#[doc = #ts_declaration_comment]
|
||||
pub struct #wrapper_name(node::RootAndChannel);
|
||||
|
||||
#[cfg(feature = "node")]
|
||||
@ -299,15 +333,29 @@ pub(crate) fn bridge_trait(trait_to_bridge: &ItemTrait, js_name: &str) -> Result
|
||||
impl #trait_name for #wrapper_name {
|
||||
#(#callback_impls)*
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "node", feature = "metadata"))]
|
||||
#[#md::linkme::distributed_slice(#md::node::NODE_ITEMS)]
|
||||
#[linkme(crate = #md::linkme)]
|
||||
static #metadata_name: #md::FnWithModule<#md::node::TsMetadataContext> = #md::FnWithModule {
|
||||
module_path: module_path!(),
|
||||
apply: |ctx| {
|
||||
let mut functions = Vec::new();
|
||||
#(#callback_bridge_trait_functions)*
|
||||
ctx.bridge_traits.insert(#js_name.to_string(), functions);
|
||||
},
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
struct Callback {
|
||||
implementation: TokenStream2,
|
||||
ts_decl: String,
|
||||
/// Push a `node::BridgeTraitFunction` onto the local `functions` Vec
|
||||
/// `ctx: &mut TsMetadataContext` is in scope
|
||||
bridge_trait_function: TokenStream2,
|
||||
}
|
||||
|
||||
fn bridge_callback_item(item: &TraitItem) -> Result<Callback> {
|
||||
fn bridge_callback_item(item: &TraitItem, krate: &TokenStream2) -> Result<Callback> {
|
||||
let TraitItem::Fn(item) = item else {
|
||||
return Err(Error::new(item.span(), "only fns are supported"));
|
||||
};
|
||||
@ -395,21 +443,35 @@ fn bridge_callback_item(item: &TraitItem) -> Result<Callback> {
|
||||
}
|
||||
};
|
||||
|
||||
// operation(foo: number): void;
|
||||
let js_arg_decls = item.sig.inputs.iter().filter_map(|arg| match arg {
|
||||
FnArg::Receiver(_) => None,
|
||||
FnArg::Typed(arg) => {
|
||||
let Pat::Ident(arg_name) = &*arg.pat else {
|
||||
// Diagnosed elsewhere.
|
||||
return None;
|
||||
};
|
||||
Some(format!("{}: {}", arg_name.ident, arg.ty.to_token_stream()))
|
||||
}
|
||||
});
|
||||
let args = item
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.filter_map(|arg| match arg {
|
||||
FnArg::Receiver(_) => None,
|
||||
FnArg::Typed(arg) => {
|
||||
let Pat::Ident(arg_name) = &*arg.pat else {
|
||||
// Diagnosed elsewhere.
|
||||
return None;
|
||||
};
|
||||
Some((&arg_name.ident, &arg.ty))
|
||||
}
|
||||
})
|
||||
.collect_vec();
|
||||
let arg_names = args
|
||||
.iter()
|
||||
.map(|(x, _)| to_lower_camel_case_preserve_underscores(&x.to_string()))
|
||||
.collect_vec();
|
||||
let arg_types = args.iter().map(|(_, x)| x).collect_vec();
|
||||
let result_ty = result_type(&sig.output);
|
||||
|
||||
let result_string = if sig.asyncness.is_some() {
|
||||
let result_ty = result_type(&sig.output);
|
||||
format!("Promise<{result_ty}>")
|
||||
let return_type = if sig.asyncness.is_some() {
|
||||
quote! {{
|
||||
use #krate::metadata::node::result_type_helper::*;
|
||||
let return_type: CallbackResultMetadataTransformHelper<#result_ty> = Default::default();
|
||||
let return_type = return_type.register_ts_ffi_type(ctx);
|
||||
format!("Promise<{return_type}>")
|
||||
}}
|
||||
} else {
|
||||
if !matches!(sig.output, ReturnType::Default) {
|
||||
return Err(Error::new(
|
||||
@ -417,17 +479,25 @@ fn bridge_callback_item(item: &TraitItem) -> Result<Callback> {
|
||||
"non-async callbacks with results are not supported for Node",
|
||||
));
|
||||
}
|
||||
"void".to_owned()
|
||||
quote!("void".to_string())
|
||||
};
|
||||
let ts_decl = format!(
|
||||
"{}({}): {};",
|
||||
js_operation_name,
|
||||
js_arg_decls.format(", "),
|
||||
result_string
|
||||
);
|
||||
|
||||
Ok(Callback {
|
||||
implementation,
|
||||
ts_decl,
|
||||
bridge_trait_function: quote! {
|
||||
let mut arguments = Vec::new();
|
||||
#(arguments.push((
|
||||
#arg_names.to_string(),
|
||||
<#arg_types as #krate::node::ResultTypeInfo>::register_ts_ffi_type(ctx),
|
||||
));)*
|
||||
let return_type = #return_type;
|
||||
functions.push(#krate::metadata::node::BridgeTraitFunction {
|
||||
name: #js_operation_name.to_string(),
|
||||
body: #krate::metadata::node::NativeFunction {
|
||||
arguments,
|
||||
return_type,
|
||||
},
|
||||
});
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -9,6 +9,20 @@ use syn::spanned::Spanned;
|
||||
use syn::*;
|
||||
use syn_mid::{FnArg, Pat, PatType, Signature};
|
||||
|
||||
pub(crate) mod crates {
|
||||
use super::*;
|
||||
fn pkg_name() -> String {
|
||||
std::env::var("CARGO_PKG_NAME").expect("Missing CARGO_PKG_NAME")
|
||||
}
|
||||
pub(crate) fn libsignal_bridge_types() -> TokenStream2 {
|
||||
if pkg_name() == "libsignal-bridge-types" {
|
||||
quote!(crate)
|
||||
} else {
|
||||
quote!(::libsignal_bridge_types)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the tokens of the type in `output_as_written`, or `()` if no return type was written.
|
||||
pub(crate) fn result_type(output_as_written: &ReturnType) -> TokenStream2 {
|
||||
match output_as_written {
|
||||
|
||||
@ -55,3 +55,4 @@ ffi = ["libsignal-bridge-types/ffi"]
|
||||
jni = ["dep:jni", "libsignal-bridge-types/jni"]
|
||||
node = ["dep:linkme", "dep:neon", "libsignal-bridge-types/node"]
|
||||
signal-media = ["libsignal-bridge-types/signal-media"]
|
||||
metadata = ["libsignal-bridge-types/metadata"]
|
||||
|
||||
@ -105,6 +105,10 @@ impl<'storage, 'context: 'storage> node::ArgTypeInfo<'storage, 'context> for Nee
|
||||
fn load_from(_stored: &'storage mut Self::StoredType) -> Self {
|
||||
Self::None
|
||||
}
|
||||
#[cfg(feature = "metadata")]
|
||||
fn register_ts_ffi_type(_: &mut metadata::node::TsMetadataContext) -> String {
|
||||
"null".into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "node")]
|
||||
@ -123,6 +127,10 @@ impl<'storage> node::AsyncArgTypeInfo<'storage> for NeedsCleanup {
|
||||
// We only want to test that the storage is cleaned up, not the value passed into the wrapped function.
|
||||
Self::None
|
||||
}
|
||||
#[cfg(feature = "metadata")]
|
||||
fn register_ts_ffi_type(_: &mut metadata::node::TsMetadataContext) -> String {
|
||||
"null".into()
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that implements ArgTypeInfo but always produces an error when "borrowed" from the
|
||||
@ -162,6 +170,10 @@ impl node::SimpleArgTypeInfo for ErrorOnBorrow {
|
||||
) -> node::NeonResult<Self> {
|
||||
node::Context::throw_type_error(cx, "deliberate error")
|
||||
}
|
||||
#[cfg(feature = "metadata")]
|
||||
fn register_ts_ffi_type(_: &mut metadata::node::TsMetadataContext) -> String {
|
||||
"null".into()
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that implements ArgTypeInfo but panics as it is "borrowed" from the app-provided
|
||||
@ -199,6 +211,11 @@ impl node::SimpleArgTypeInfo for PanicOnBorrow {
|
||||
) -> node::NeonResult<Self> {
|
||||
panic!("deliberate panic")
|
||||
}
|
||||
|
||||
#[cfg(feature = "metadata")]
|
||||
fn register_ts_ffi_type(_: &mut metadata::node::TsMetadataContext) -> String {
|
||||
"null".into()
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that implements ArgTypeInfo but panics on the secondary "load" step after the "borrow"
|
||||
@ -258,6 +275,11 @@ impl<'storage, 'context: 'storage> node::ArgTypeInfo<'storage, 'context> for Pan
|
||||
fn load_from(_stored: &'storage mut Self::StoredType) -> Self {
|
||||
panic!("deliberate panic")
|
||||
}
|
||||
|
||||
#[cfg(feature = "metadata")]
|
||||
fn register_ts_ffi_type(_: &mut metadata::node::TsMetadataContext) -> String {
|
||||
"null".into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "node")]
|
||||
@ -276,6 +298,11 @@ impl<'storage> node::AsyncArgTypeInfo<'storage> for PanicOnLoad {
|
||||
fn load_async_arg(_stored: &'storage mut Self::StoredType) -> Self {
|
||||
panic!("deliberate panic")
|
||||
}
|
||||
|
||||
#[cfg(feature = "metadata")]
|
||||
fn register_ts_ffi_type(_: &mut metadata::node::TsMetadataContext) -> String {
|
||||
"null".into()
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that implements ResultTypeInfo but always fails to produce a result.
|
||||
@ -311,6 +338,11 @@ impl<'a> node::ResultTypeInfo<'a> for ErrorOnReturn {
|
||||
fn convert_into(self, cx: &mut impl node::Context<'a>) -> node::JsResult<'a, Self::ResultType> {
|
||||
cx.throw_type_error("deliberate error")
|
||||
}
|
||||
|
||||
#[cfg(feature = "metadata")]
|
||||
fn register_ts_ffi_type(_: &mut metadata::node::TsMetadataContext) -> String {
|
||||
"null".into()
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that implements ResultTypeInfo but always panics when producing a result.
|
||||
@ -347,6 +379,11 @@ impl<'a> node::ResultTypeInfo<'a> for PanicOnReturn {
|
||||
) -> node::JsResult<'a, Self::ResultType> {
|
||||
panic!("deliberate panic");
|
||||
}
|
||||
|
||||
#[cfg(feature = "metadata")]
|
||||
fn register_ts_ffi_type(_: &mut metadata::node::TsMetadataContext) -> String {
|
||||
"null".into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(derive_more::Deref)]
|
||||
@ -430,6 +467,11 @@ impl<'storage> node::AsyncArgTypeInfo<'storage> for TestingFutureCancellationGua
|
||||
fn load_async_arg(stored: &'storage mut Self::StoredType) -> Self {
|
||||
stored.take().unwrap().0
|
||||
}
|
||||
|
||||
#[cfg(feature = "metadata")]
|
||||
fn register_ts_ffi_type(_: &mut metadata::node::TsMetadataContext) -> String {
|
||||
"TestingFutureCancellationGuard".into()
|
||||
}
|
||||
}
|
||||
|
||||
bridge_as_handle!(TestingFutureCancellationCounter);
|
||||
|
||||
@ -84,6 +84,7 @@ jni-type-tagging = []
|
||||
jni-invoke-annotated = []
|
||||
extra-jni-checks = ["jni-type-tagging", "jni-invoke-annotated"]
|
||||
node = ["neon", "linkme", "signal-neon-futures"]
|
||||
metadata = ["linkme", "serde/derive"]
|
||||
|
||||
[target.'cfg(not(any(windows, target_arch = "x86")))'.dependencies]
|
||||
# sha2's asm implementation uses standalone .S files that aren't compiled correctly on Windows,
|
||||
|
||||
@ -6,6 +6,9 @@
|
||||
#![allow(clippy::missing_safety_doc)]
|
||||
#![deny(clippy::unwrap_used)]
|
||||
|
||||
#[cfg(feature = "metadata")]
|
||||
pub mod metadata;
|
||||
|
||||
#[cfg(feature = "ffi")]
|
||||
#[macro_use]
|
||||
pub mod ffi;
|
||||
|
||||
108
rust/bridge/shared/types/src/metadata.rs
Normal file
108
rust/bridge/shared/types/src/metadata.rs
Normal file
@ -0,0 +1,108 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
//! This module provides metadata about the bridge layer which will be consumed downstream for
|
||||
//! various purposes:
|
||||
//!
|
||||
//! - To emit `Native.ts`, see `libsignal-node-native_ts`
|
||||
//!
|
||||
//! While some metadata facilities are shared, they're specialized to each client language.
|
||||
|
||||
// This is pub so that it can be used in bridge macros.
|
||||
pub use linkme;
|
||||
use linkme::distributed_slice;
|
||||
use serde::Serialize;
|
||||
|
||||
#[cfg(feature = "node")]
|
||||
pub mod node {
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Default)]
|
||||
pub struct TsMetadataContext {
|
||||
pub opaque_types: BTreeSet<String>,
|
||||
pub native_functions: BTreeMap<String, NativeFunction>,
|
||||
pub bridge_traits: BTreeMap<String, Vec<BridgeTraitFunction>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct NativeFunction {
|
||||
/// (name, type)
|
||||
pub arguments: Vec<(String, String)>,
|
||||
pub return_type: String,
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct BridgeTraitFunction {
|
||||
pub name: String,
|
||||
pub body: NativeFunction,
|
||||
}
|
||||
|
||||
/// These functions should mutate the attached [TsMetadataContext] to register their item.
|
||||
#[distributed_slice]
|
||||
pub static NODE_ITEMS: [FnWithModule<TsMetadataContext>];
|
||||
|
||||
/// See [crate::support]'s `transform_helper` for how this works, and the rationale.
|
||||
///
|
||||
/// These functions provide the metadata-side (`register_ts_ffi_type()`) of `.ok_if_needed()`
|
||||
///
|
||||
/// ```
|
||||
/// # use libsignal_bridge_types::metadata::node::result_type_helper::*;
|
||||
/// let x: ResultMetadataTransformHelper<i32> = Default::default();
|
||||
/// assert_eq!(x.register_ts_ffi_type(&mut Default::default()).as_str(), "number");
|
||||
/// let y: ResultMetadataTransformHelper<Result<i32, String>> = Default::default();
|
||||
/// assert_eq!(y.register_ts_ffi_type(&mut Default::default()).as_str(), "number");
|
||||
/// ```
|
||||
pub mod result_type_helper {
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use derive_where::derive_where;
|
||||
|
||||
use crate::metadata::node::TsMetadataContext;
|
||||
use crate::node::{CallbackResultTypeInfo, ResultTypeInfo};
|
||||
|
||||
#[derive_where(Default)]
|
||||
pub struct ResultMetadataTransformHelper<T>(PhantomData<T>);
|
||||
impl<'a, T: ResultTypeInfo<'a>> ResultMetadataTransformHelper<T> {
|
||||
pub fn register_ts_ffi_type(&self, ctx: &mut TsMetadataContext) -> String {
|
||||
T::register_ts_ffi_type(ctx)
|
||||
}
|
||||
}
|
||||
pub trait ResultMetadataTransformHelperTrait {
|
||||
fn register_ts_ffi_type(&self, ctx: &mut TsMetadataContext) -> String;
|
||||
}
|
||||
impl<'a, T: ResultTypeInfo<'a>, E> ResultMetadataTransformHelperTrait
|
||||
for ResultMetadataTransformHelper<Result<T, E>>
|
||||
{
|
||||
fn register_ts_ffi_type(&self, ctx: &mut TsMetadataContext) -> String {
|
||||
T::register_ts_ffi_type(ctx)
|
||||
}
|
||||
}
|
||||
#[derive_where(Default)]
|
||||
pub struct CallbackResultMetadataTransformHelper<T>(PhantomData<T>);
|
||||
|
||||
impl<T: CallbackResultTypeInfo> CallbackResultMetadataTransformHelper<T> {
|
||||
pub fn register_ts_ffi_type(&self, ctx: &mut TsMetadataContext) -> String {
|
||||
T::register_ts_ffi_type(ctx)
|
||||
}
|
||||
}
|
||||
pub trait CallbackResultMetadataTransformHelperTrait {
|
||||
fn register_ts_ffi_type(&self, ctx: &mut TsMetadataContext) -> String;
|
||||
}
|
||||
impl<T: CallbackResultTypeInfo, E> CallbackResultMetadataTransformHelperTrait
|
||||
for CallbackResultMetadataTransformHelper<Result<T, E>>
|
||||
{
|
||||
fn register_ts_ffi_type(&self, ctx: &mut TsMetadataContext) -> String {
|
||||
T::register_ts_ffi_type(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FnWithModule<Ctx> {
|
||||
/// The module the function is defined in
|
||||
pub module_path: &'static str,
|
||||
pub apply: fn(&mut Ctx),
|
||||
}
|
||||
@ -81,7 +81,6 @@ macro_rules! define_keys {
|
||||
}
|
||||
|
||||
impl RemoteConfigKey {
|
||||
#[doc = concat!("ts: `export const NetRemoteConfigKeys = [", $("'", $key, "', "),* ,"] as const;`")]
|
||||
pub const KEYS: &[&str] = &[$($key),*];
|
||||
#[cfg(test)]
|
||||
const IDENTITIER_KEY_PAIRS: &[(&str, &str)] = &[
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -15,6 +15,9 @@ impl<T, const LEN: usize> Array<T> for [T; LEN] {
|
||||
pub trait FixedLengthBincodeSerializable: 'static {
|
||||
/// Should be an actual byte array type, like `[u8; 7]`.
|
||||
type Array: Array<u8> + for<'a> TryFrom<&'a [u8], Error = std::array::TryFromSliceError>;
|
||||
|
||||
#[cfg(feature = "metadata")]
|
||||
fn name() -> String;
|
||||
}
|
||||
|
||||
/// A wrapper type that indicates that `T` should be serialized across the bridges.
|
||||
|
||||
@ -32,11 +32,13 @@ pub fn validate_serialization<'a, T: Deserialize<'a> + PartialDefault>(
|
||||
macro_rules! bridge_as_fixed_length_serializable {
|
||||
($typ:ident) => {
|
||||
::paste::paste! {
|
||||
// Declare a marker type for TypeScript, the same as bridge_as_handle.
|
||||
// (This is harmless for the other bridges.)
|
||||
#[doc = "ts: `interface " $typ " { readonly __type: unique symbol; }`"]
|
||||
impl FixedLengthBincodeSerializable for $typ {
|
||||
type Array = [u8; [<$typ:snake:upper _LEN>]];
|
||||
#[cfg(feature = "metadata")]
|
||||
fn name() -> String {
|
||||
let name = stringify!($typ);
|
||||
name.rsplit_once("::").map(|(_, x)| x).unwrap_or(name).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user