@tishlang/tish-format 3.12.2 → 3.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +1 -0
- package/bin/tish-format +0 -0
- package/crates/tish/Cargo.toml +3 -1
- package/crates/tish/src/cli_help.rs +2 -1
- package/crates/tish/src/main.rs +21 -2
- package/crates/tish_bytecode/src/compiler.rs +6 -8
- package/crates/tish_compile/src/codegen.rs +93 -6
- package/crates/tish_compile/src/lib.rs +4 -0
- package/crates/tish_compile/src/resolve.rs +1 -0
- package/crates/tish_compile/tests/regr_ffi_module_emit.rs +120 -0
- package/crates/tish_core/src/lib.rs +43 -4
- package/crates/tish_cranelift_runtime/Cargo.toml +2 -0
- package/crates/tish_ffi/src/lib.rs +48 -25
- package/crates/tish_ffi/src/v2.rs +349 -0
- package/crates/tish_ffi/tests/fixtures/testmod_v2/Cargo.lock +282 -0
- package/crates/tish_ffi/tests/fixtures/testmod_v2/Cargo.toml +16 -0
- package/crates/tish_ffi/tests/fixtures/testmod_v2/src/lib.rs +111 -0
- package/crates/tish_ffi/tests/loader_v2.rs +159 -0
- package/crates/tish_ffi_guest/Cargo.toml +11 -0
- package/crates/tish_ffi_guest/src/lib.rs +333 -0
- package/crates/tish_fmt/Cargo.toml +1 -1
- package/crates/tish_native/src/build.rs +54 -15
- package/crates/tish_native/src/config.rs +10 -0
- package/crates/tish_native/src/lib.rs +7 -2
- package/crates/tish_runtime/Cargo.toml +4 -0
- package/crates/tish_runtime/src/lib.rs +21 -0
- package/crates/tish_vm/Cargo.toml +2 -0
- package/crates/tish_vm/src/vm.rs +61 -6
- package/crates/tish_vm/tests/ffi_load_module.rs +32 -0
- package/crates/tish_vm/tests/jit_off_leaves_jit_state.rs +24 -0
- package/crates/tish_vm/tests/jsx_lexical_resolution.rs +42 -0
- package/crates/tish_vm/tests/multi_vm_isolation.rs +123 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-fmt +0 -0
- package/platform/darwin-x64/tish-fmt +0 -0
- package/platform/linux-arm64/tish-fmt +0 -0
- package/platform/linux-x64/tish-fmt +0 -0
- package/platform/win32-x64/tish-fmt.exe +0 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
use tishlang_core::{take_pending_throw, Value, VmRef};
|
|
2
|
+
use tishlang_ffi_guest as guest;
|
|
3
|
+
use guest::{TishExportTable, TishHostApi, TishValueRef};
|
|
4
|
+
|
|
5
|
+
fn no_throw() -> Value {
|
|
6
|
+
Value::Null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
fn parked_throw() -> Value {
|
|
10
|
+
take_pending_throw().unwrap_or(Value::Null)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/// `describe(obj)` → "k1,k2|<count>": reads a host object's keys (v1 could not).
|
|
14
|
+
extern "C" fn describe(args: *const TishValueRef, argc: usize) -> TishValueRef {
|
|
15
|
+
unsafe {
|
|
16
|
+
guest::export_call(
|
|
17
|
+
args,
|
|
18
|
+
argc,
|
|
19
|
+
|a| match a.first() {
|
|
20
|
+
Some(Value::Object(o)) => {
|
|
21
|
+
let o = o.borrow();
|
|
22
|
+
let keys: Vec<String> = o.strings.keys().map(|k| k.to_string()).collect();
|
|
23
|
+
Value::String(format!("{}|{}", keys.join(","), keys.len()).into())
|
|
24
|
+
}
|
|
25
|
+
_ => Value::Null,
|
|
26
|
+
},
|
|
27
|
+
no_throw,
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// `apply(f, x)` → `f(x) + 1`: calls a host function from the module.
|
|
33
|
+
extern "C" fn apply(args: *const TishValueRef, argc: usize) -> TishValueRef {
|
|
34
|
+
unsafe {
|
|
35
|
+
guest::export_call(
|
|
36
|
+
args,
|
|
37
|
+
argc,
|
|
38
|
+
|a| match (a.first(), a.get(1)) {
|
|
39
|
+
(Some(Value::Function(f)), Some(x)) => match f.call(&[x.clone()]) {
|
|
40
|
+
Value::Number(n) => Value::Number(n + 1.0),
|
|
41
|
+
other => other,
|
|
42
|
+
},
|
|
43
|
+
_ => Value::Null,
|
|
44
|
+
},
|
|
45
|
+
parked_throw,
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// `make_adder(n)` → a module function `(x) => x + n` the host can call later.
|
|
51
|
+
extern "C" fn make_adder(args: *const TishValueRef, argc: usize) -> TishValueRef {
|
|
52
|
+
unsafe {
|
|
53
|
+
guest::export_call(
|
|
54
|
+
args,
|
|
55
|
+
argc,
|
|
56
|
+
|a| {
|
|
57
|
+
let n = match a.first() {
|
|
58
|
+
Some(Value::Number(n)) => *n,
|
|
59
|
+
_ => 0.0,
|
|
60
|
+
};
|
|
61
|
+
Value::native(move |xs: &[Value]| match xs.first() {
|
|
62
|
+
Some(Value::Number(x)) => Value::Number(x + n),
|
|
63
|
+
_ => Value::Null,
|
|
64
|
+
})
|
|
65
|
+
},
|
|
66
|
+
no_throw,
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// `boom(msg)` throws `msg` into the host.
|
|
72
|
+
extern "C" fn boom(args: *const TishValueRef, argc: usize) -> TishValueRef {
|
|
73
|
+
unsafe {
|
|
74
|
+
let msg = std::cell::RefCell::new(Value::Null);
|
|
75
|
+
guest::export_call(
|
|
76
|
+
args,
|
|
77
|
+
argc,
|
|
78
|
+
|a| {
|
|
79
|
+
*msg.borrow_mut() = a.first().cloned().unwrap_or(Value::Null);
|
|
80
|
+
Value::Null
|
|
81
|
+
},
|
|
82
|
+
|| msg.borrow().clone(),
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// `echo(x)` returns a deep copy of `x` (arrays, nested objects, strings with NUL).
|
|
88
|
+
extern "C" fn echo(args: *const TishValueRef, argc: usize) -> TishValueRef {
|
|
89
|
+
unsafe {
|
|
90
|
+
guest::export_call(
|
|
91
|
+
args,
|
|
92
|
+
argc,
|
|
93
|
+
|a| a.first().cloned().unwrap_or(Value::Array(VmRef::new(Vec::new()))),
|
|
94
|
+
no_throw,
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#[no_mangle]
|
|
100
|
+
pub unsafe extern "C" fn tish_module_register_v2(api: *const TishHostApi) -> *const TishExportTable {
|
|
101
|
+
if !guest::install(api) {
|
|
102
|
+
return std::ptr::null();
|
|
103
|
+
}
|
|
104
|
+
guest::export_table(&[
|
|
105
|
+
(b"describe\0", describe),
|
|
106
|
+
(b"apply\0", apply),
|
|
107
|
+
(b"make_adder\0", make_adder),
|
|
108
|
+
(b"boom\0", boom),
|
|
109
|
+
(b"echo\0", echo),
|
|
110
|
+
])
|
|
111
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
//! ABI v2 end to end: a cdylib that links no host symbols receives the `TishHostApi` table via
|
|
2
|
+
//! `tish_module_register_v2`, and objects, callbacks (both directions) and throws cross it.
|
|
3
|
+
|
|
4
|
+
#![cfg(not(target_arch = "wasm32"))]
|
|
5
|
+
|
|
6
|
+
use std::path::PathBuf;
|
|
7
|
+
use std::process::Command;
|
|
8
|
+
|
|
9
|
+
use tishlang_core::{take_pending_throw, ObjectData, Value, VmRef};
|
|
10
|
+
|
|
11
|
+
fn build_and_locate_fixture() -> PathBuf {
|
|
12
|
+
let manifest = format!(
|
|
13
|
+
"{}/tests/fixtures/testmod_v2/Cargo.toml",
|
|
14
|
+
env!("CARGO_MANIFEST_DIR")
|
|
15
|
+
);
|
|
16
|
+
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
|
|
17
|
+
let status = Command::new(cargo)
|
|
18
|
+
.args(["build", "--release", "--manifest-path", &manifest])
|
|
19
|
+
.status()
|
|
20
|
+
.expect("spawn cargo to build v2 fixture cdylib");
|
|
21
|
+
assert!(status.success(), "v2 fixture cdylib build failed");
|
|
22
|
+
|
|
23
|
+
let dir = format!(
|
|
24
|
+
"{}/tests/fixtures/testmod_v2/target/release",
|
|
25
|
+
env!("CARGO_MANIFEST_DIR")
|
|
26
|
+
);
|
|
27
|
+
std::fs::read_dir(&dir)
|
|
28
|
+
.unwrap_or_else(|e| panic!("read {dir}: {e}"))
|
|
29
|
+
.filter_map(|e| e.ok().map(|e| e.path()))
|
|
30
|
+
.find(|p| {
|
|
31
|
+
let name = p.file_name().unwrap_or_default().to_string_lossy();
|
|
32
|
+
name.contains("tish_ffi_testmod_v2")
|
|
33
|
+
&& matches!(
|
|
34
|
+
p.extension().and_then(|x| x.to_str()),
|
|
35
|
+
Some("dylib") | Some("so") | Some("dll")
|
|
36
|
+
)
|
|
37
|
+
})
|
|
38
|
+
.expect("built v2 cdylib artifact")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fn export(name: &str) -> tishlang_core::NativeFn {
|
|
42
|
+
let lib = build_and_locate_fixture();
|
|
43
|
+
let module = tishlang_ffi::load_module(lib.to_str().unwrap())
|
|
44
|
+
.unwrap_or_else(|e| panic!("load_module: {e}"));
|
|
45
|
+
match module.get(name) {
|
|
46
|
+
Some(Value::Function(f)) => f.clone(),
|
|
47
|
+
other => panic!("{name} export = {other:?}"),
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fn obj(pairs: &[(&str, Value)]) -> Value {
|
|
52
|
+
let mut o = ObjectData::default();
|
|
53
|
+
for (k, v) in pairs {
|
|
54
|
+
o.strings.insert((*k).into(), v.clone());
|
|
55
|
+
}
|
|
56
|
+
Value::Object(VmRef::new(o))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#[test]
|
|
60
|
+
fn host_object_keys_are_readable_in_module() {
|
|
61
|
+
let describe = export("describe");
|
|
62
|
+
let out = describe.call(&[obj(&[("title", Value::String("Safari".into())), ("rank", Value::Number(1.0))])]);
|
|
63
|
+
match out {
|
|
64
|
+
Value::String(s) => assert_eq!(s.as_str(), "title,rank|2"),
|
|
65
|
+
other => panic!("describe = {other:?}"),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[test]
|
|
70
|
+
fn module_calls_host_function() {
|
|
71
|
+
let apply = export("apply");
|
|
72
|
+
let double = Value::native(|a: &[Value]| match a.first() {
|
|
73
|
+
Some(Value::Number(n)) => Value::Number(n * 2.0),
|
|
74
|
+
_ => Value::Null,
|
|
75
|
+
});
|
|
76
|
+
match apply.call(&[double, Value::Number(20.0)]) {
|
|
77
|
+
Value::Number(n) => assert_eq!(n, 41.0),
|
|
78
|
+
other => panic!("apply = {other:?}"),
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[test]
|
|
83
|
+
fn host_calls_module_function() {
|
|
84
|
+
let make_adder = export("make_adder");
|
|
85
|
+
let add5 = make_adder.call(&[Value::Number(5.0)]);
|
|
86
|
+
let Value::Function(add5) = add5 else {
|
|
87
|
+
panic!("make_adder returned {add5:?}");
|
|
88
|
+
};
|
|
89
|
+
match add5.call(&[Value::Number(10.0)]) {
|
|
90
|
+
Value::Number(n) => assert_eq!(n, 15.0),
|
|
91
|
+
other => panic!("add5(10) = {other:?}"),
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#[test]
|
|
96
|
+
fn module_throw_reaches_host() {
|
|
97
|
+
let boom = export("boom");
|
|
98
|
+
let _ = take_pending_throw();
|
|
99
|
+
let out = boom.call(&[Value::String("plugin failed".into())]);
|
|
100
|
+
assert!(matches!(out, Value::Null));
|
|
101
|
+
match take_pending_throw() {
|
|
102
|
+
Some(Value::String(s)) => assert_eq!(s.as_str(), "plugin failed"),
|
|
103
|
+
other => panic!("pending throw = {other:?}"),
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#[test]
|
|
108
|
+
fn host_throw_inside_callback_propagates_through_module() {
|
|
109
|
+
let apply = export("apply");
|
|
110
|
+
let thrower = Value::native(|_a: &[Value]| {
|
|
111
|
+
tishlang_core::set_pending_throw(Value::String("host callback failed".into()));
|
|
112
|
+
Value::Null
|
|
113
|
+
});
|
|
114
|
+
let _ = take_pending_throw();
|
|
115
|
+
let out = apply.call(&[thrower, Value::Number(1.0)]);
|
|
116
|
+
assert!(matches!(out, Value::Null));
|
|
117
|
+
match take_pending_throw() {
|
|
118
|
+
Some(Value::String(s)) => assert_eq!(s.as_str(), "host callback failed"),
|
|
119
|
+
other => panic!("pending throw = {other:?}"),
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#[test]
|
|
124
|
+
fn nested_values_and_nul_strings_round_trip() {
|
|
125
|
+
let echo = export("echo");
|
|
126
|
+
let input = obj(&[
|
|
127
|
+
("name", Value::String("a\0b".into())),
|
|
128
|
+
(
|
|
129
|
+
"items",
|
|
130
|
+
Value::Array(VmRef::new(vec![
|
|
131
|
+
Value::Number(1.0),
|
|
132
|
+
obj(&[("ok", Value::Bool(true))]),
|
|
133
|
+
Value::Null,
|
|
134
|
+
])),
|
|
135
|
+
),
|
|
136
|
+
]);
|
|
137
|
+
let out = echo.call(&[input]);
|
|
138
|
+
let Value::Object(o) = out else {
|
|
139
|
+
panic!("echo = {out:?}");
|
|
140
|
+
};
|
|
141
|
+
let o = o.borrow();
|
|
142
|
+
match o.strings.get("name") {
|
|
143
|
+
Some(Value::String(s)) => assert_eq!(s.as_str(), "a\0b"),
|
|
144
|
+
other => panic!("name = {other:?}"),
|
|
145
|
+
}
|
|
146
|
+
match o.strings.get("items") {
|
|
147
|
+
Some(Value::Array(a)) => {
|
|
148
|
+
let a = a.borrow();
|
|
149
|
+
assert_eq!(a.len(), 3);
|
|
150
|
+
match &a[1] {
|
|
151
|
+
Value::Object(inner) => {
|
|
152
|
+
assert!(matches!(inner.borrow().strings.get("ok"), Some(Value::Bool(true))))
|
|
153
|
+
}
|
|
154
|
+
other => panic!("items[1] = {other:?}"),
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
other => panic!("items = {other:?}"),
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "tishlang_ffi_guest"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
description = "Module-side half of the tish FFI ABI v2: deep-converts between the host's opaque handles (through the TishHostApi table) and the module's own tishlang_core values, including functions in both directions. Links no host symbols."
|
|
6
|
+
|
|
7
|
+
license-file = { workspace = true }
|
|
8
|
+
repository = { workspace = true }
|
|
9
|
+
|
|
10
|
+
[dependencies]
|
|
11
|
+
tishlang_core = { path = "../tish_core", version = ">=0.1" }
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
//! # Module side of the tish FFI ABI v2
|
|
2
|
+
//!
|
|
3
|
+
//! A native module (cdylib) exports
|
|
4
|
+
//! `tish_module_register_v2(api: *const TishHostApi) -> *const TishExportTable`. It passes `api`
|
|
5
|
+
//! to [`install`], then each export trampoline calls [`export_call`], which deep-converts the
|
|
6
|
+
//! host's argument handles into the module's own [`Value`]s, runs the export, and converts the
|
|
7
|
+
//! result (or the export's throw) back.
|
|
8
|
+
//!
|
|
9
|
+
//! The module links its own `tishlang_core`; nothing here names a host symbol, so no linker flags
|
|
10
|
+
//! are needed on either side. Functions cross in both directions: a host function becomes a local
|
|
11
|
+
//! `Value::Function` that calls back through `api.call`, and a local function becomes a host
|
|
12
|
+
//! function via `api.new_function`. Calls happen on the calling thread.
|
|
13
|
+
//!
|
|
14
|
+
//! The `#[repr(C)]` types mirror `tishlang_ffi`'s host definitions field for field; the ABI is
|
|
15
|
+
//! the contract, and `abi_version` / `size` guard every field newer than v2.
|
|
16
|
+
|
|
17
|
+
use std::ffi::{c_char, c_void};
|
|
18
|
+
use std::sync::OnceLock;
|
|
19
|
+
|
|
20
|
+
use tishlang_core::{ObjectData, Value, VmRef};
|
|
21
|
+
|
|
22
|
+
pub type TishValueRef = *mut c_void;
|
|
23
|
+
pub type TishNativeFn = extern "C" fn(args: *const TishValueRef, argc: usize) -> TishValueRef;
|
|
24
|
+
pub type TishCallbackFn =
|
|
25
|
+
extern "C" fn(ctx: *mut c_void, args: *const TishValueRef, argc: usize) -> TishValueRef;
|
|
26
|
+
pub type TishCtxDropFn = extern "C" fn(ctx: *mut c_void);
|
|
27
|
+
|
|
28
|
+
pub const TISH_FFI_ABI_VERSION: u32 = 2;
|
|
29
|
+
pub const TISH_TAG_NULL: i32 = 0;
|
|
30
|
+
pub const TISH_TAG_NUMBER: i32 = 1;
|
|
31
|
+
pub const TISH_TAG_STRING: i32 = 2;
|
|
32
|
+
pub const TISH_TAG_BOOL: i32 = 3;
|
|
33
|
+
pub const TISH_TAG_ARRAY: i32 = 4;
|
|
34
|
+
pub const TISH_TAG_OBJECT: i32 = 5;
|
|
35
|
+
pub const TISH_TAG_OTHER: i32 = 6;
|
|
36
|
+
pub const TISH_TAG_FUNCTION: i32 = 7;
|
|
37
|
+
|
|
38
|
+
#[repr(C)]
|
|
39
|
+
pub struct TishExport {
|
|
40
|
+
pub name: *const c_char,
|
|
41
|
+
pub func: TishNativeFn,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#[repr(C)]
|
|
45
|
+
pub struct TishExportTable {
|
|
46
|
+
pub exports: *const TishExport,
|
|
47
|
+
pub count: usize,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// SAFETY: the table only holds pointers to `'static` names and fn pointers.
|
|
51
|
+
unsafe impl Sync for TishExportTable {}
|
|
52
|
+
unsafe impl Send for TishExportTable {}
|
|
53
|
+
|
|
54
|
+
#[repr(C)]
|
|
55
|
+
pub struct TishHostApi {
|
|
56
|
+
pub abi_version: u32,
|
|
57
|
+
pub size: usize,
|
|
58
|
+
pub new_null: extern "C" fn() -> TishValueRef,
|
|
59
|
+
pub new_number: extern "C" fn(n: f64) -> TishValueRef,
|
|
60
|
+
pub new_bool: extern "C" fn(b: bool) -> TishValueRef,
|
|
61
|
+
pub new_string_utf8: unsafe extern "C" fn(ptr: *const u8, len: usize) -> TishValueRef,
|
|
62
|
+
pub tag: unsafe extern "C" fn(r: TishValueRef) -> i32,
|
|
63
|
+
pub as_number: unsafe extern "C" fn(r: TishValueRef) -> f64,
|
|
64
|
+
pub as_bool: unsafe extern "C" fn(r: TishValueRef) -> bool,
|
|
65
|
+
pub string_utf8: unsafe extern "C" fn(r: TishValueRef, len: *mut usize) -> *const u8,
|
|
66
|
+
pub array_new: extern "C" fn() -> TishValueRef,
|
|
67
|
+
pub array_push: unsafe extern "C" fn(arr: TishValueRef, elem: TishValueRef),
|
|
68
|
+
pub array_len: unsafe extern "C" fn(arr: TishValueRef) -> usize,
|
|
69
|
+
pub array_get: unsafe extern "C" fn(arr: TishValueRef, i: usize) -> TishValueRef,
|
|
70
|
+
pub array_set: unsafe extern "C" fn(arr: TishValueRef, i: usize, elem: TishValueRef),
|
|
71
|
+
pub object_new: extern "C" fn() -> TishValueRef,
|
|
72
|
+
pub object_set_utf8:
|
|
73
|
+
unsafe extern "C" fn(obj: TishValueRef, key: *const u8, key_len: usize, val: TishValueRef),
|
|
74
|
+
pub object_get_utf8:
|
|
75
|
+
unsafe extern "C" fn(obj: TishValueRef, key: *const u8, key_len: usize) -> TishValueRef,
|
|
76
|
+
pub object_keys: unsafe extern "C" fn(obj: TishValueRef) -> TishValueRef,
|
|
77
|
+
pub new_function: unsafe extern "C" fn(
|
|
78
|
+
func: TishCallbackFn,
|
|
79
|
+
ctx: *mut c_void,
|
|
80
|
+
drop_ctx: Option<TishCtxDropFn>,
|
|
81
|
+
) -> TishValueRef,
|
|
82
|
+
pub call:
|
|
83
|
+
unsafe extern "C" fn(f: TishValueRef, args: *const TishValueRef, argc: usize) -> TishValueRef,
|
|
84
|
+
pub throw_value: unsafe extern "C" fn(val: TishValueRef),
|
|
85
|
+
pub take_throw: extern "C" fn() -> TishValueRef,
|
|
86
|
+
pub clone: unsafe extern "C" fn(r: TishValueRef) -> TishValueRef,
|
|
87
|
+
pub drop: unsafe extern "C" fn(r: TishValueRef),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
static API: OnceLock<&'static TishHostApi> = OnceLock::new();
|
|
91
|
+
|
|
92
|
+
/// Record the host's table. Returns `false` (and the module should return a null export table)
|
|
93
|
+
/// if the host is older than v2 or its table is smaller than this module expects.
|
|
94
|
+
///
|
|
95
|
+
/// # Safety
|
|
96
|
+
/// `api` must be null or point to a host table that lives for the rest of the process.
|
|
97
|
+
pub unsafe fn install(api: *const TishHostApi) -> bool {
|
|
98
|
+
let Some(api) = api.as_ref() else {
|
|
99
|
+
return false;
|
|
100
|
+
};
|
|
101
|
+
if api.abi_version < TISH_FFI_ABI_VERSION || api.size < std::mem::size_of::<TishHostApi>() {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
let _ = API.set(api);
|
|
105
|
+
true
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// The installed host table. Panics if called before [`install`] succeeded.
|
|
109
|
+
pub fn api() -> &'static TishHostApi {
|
|
110
|
+
API.get().expect("tish ffi guest: host api not installed")
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// Build a leaked export table for `tish_module_register_v2` to return.
|
|
114
|
+
pub fn export_table(exports: &[(&'static [u8], TishNativeFn)]) -> *const TishExportTable {
|
|
115
|
+
let entries: Vec<TishExport> = exports
|
|
116
|
+
.iter()
|
|
117
|
+
.map(|(name, func)| TishExport {
|
|
118
|
+
name: name.as_ptr() as *const c_char,
|
|
119
|
+
func: *func,
|
|
120
|
+
})
|
|
121
|
+
.collect();
|
|
122
|
+
let entries = Box::leak(entries.into_boxed_slice());
|
|
123
|
+
Box::leak(Box::new(TishExportTable {
|
|
124
|
+
exports: entries.as_ptr(),
|
|
125
|
+
count: entries.len(),
|
|
126
|
+
}))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// Run one export: convert args, call `f`, and convert its result. If the export threw (as
|
|
130
|
+
/// reported by `last_throw`, the emitted module's `tish_last_throw`), the throw is raised in the
|
|
131
|
+
/// host and a null handle is returned.
|
|
132
|
+
///
|
|
133
|
+
/// # Safety
|
|
134
|
+
/// `args` must point to `argc` live host handles; [`install`] must have succeeded.
|
|
135
|
+
pub unsafe fn export_call(
|
|
136
|
+
args: *const TishValueRef,
|
|
137
|
+
argc: usize,
|
|
138
|
+
f: impl FnOnce(Vec<Value>) -> Value,
|
|
139
|
+
last_throw: impl FnOnce() -> Value,
|
|
140
|
+
) -> TishValueRef {
|
|
141
|
+
let argv: Vec<Value> = if args.is_null() {
|
|
142
|
+
Vec::new()
|
|
143
|
+
} else {
|
|
144
|
+
std::slice::from_raw_parts(args, argc)
|
|
145
|
+
.iter()
|
|
146
|
+
.map(|h| from_host(*h))
|
|
147
|
+
.collect()
|
|
148
|
+
};
|
|
149
|
+
let out = f(argv);
|
|
150
|
+
let thrown = last_throw();
|
|
151
|
+
if !matches!(thrown, Value::Null) {
|
|
152
|
+
raise_in_host(&thrown);
|
|
153
|
+
return (api().new_null)();
|
|
154
|
+
}
|
|
155
|
+
to_host(&out)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
unsafe fn raise_in_host(thrown: &Value) {
|
|
159
|
+
let api = api();
|
|
160
|
+
let h = to_host(thrown);
|
|
161
|
+
(api.throw_value)(h);
|
|
162
|
+
(api.drop)(h);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// Host handle → module value (deep copy; functions become callbacks into the host).
|
|
166
|
+
///
|
|
167
|
+
/// # Safety
|
|
168
|
+
/// `h` must be null or a live host handle; [`install`] must have succeeded.
|
|
169
|
+
pub unsafe fn from_host(h: TishValueRef) -> Value {
|
|
170
|
+
let api = api();
|
|
171
|
+
match (api.tag)(h) {
|
|
172
|
+
TISH_TAG_NUMBER => Value::Number((api.as_number)(h)),
|
|
173
|
+
TISH_TAG_BOOL => Value::Bool((api.as_bool)(h)),
|
|
174
|
+
TISH_TAG_STRING => {
|
|
175
|
+
let mut len = 0usize;
|
|
176
|
+
let p = (api.string_utf8)(h, &mut len);
|
|
177
|
+
if p.is_null() {
|
|
178
|
+
return Value::Null;
|
|
179
|
+
}
|
|
180
|
+
let bytes = std::slice::from_raw_parts(p, len);
|
|
181
|
+
Value::String(String::from_utf8_lossy(bytes).as_ref().into())
|
|
182
|
+
}
|
|
183
|
+
TISH_TAG_ARRAY => {
|
|
184
|
+
let n = (api.array_len)(h);
|
|
185
|
+
let mut out = Vec::with_capacity(n);
|
|
186
|
+
for i in 0..n {
|
|
187
|
+
let e = (api.array_get)(h, i);
|
|
188
|
+
out.push(from_host(e));
|
|
189
|
+
(api.drop)(e);
|
|
190
|
+
}
|
|
191
|
+
Value::Array(VmRef::new(out))
|
|
192
|
+
}
|
|
193
|
+
TISH_TAG_OBJECT => {
|
|
194
|
+
let keys = (api.object_keys)(h);
|
|
195
|
+
let n = (api.array_len)(keys);
|
|
196
|
+
let mut obj = ObjectData::default();
|
|
197
|
+
for i in 0..n {
|
|
198
|
+
let k = (api.array_get)(keys, i);
|
|
199
|
+
if let Value::String(key) = from_host(k) {
|
|
200
|
+
let b = key.as_bytes();
|
|
201
|
+
let v = (api.object_get_utf8)(h, b.as_ptr(), b.len());
|
|
202
|
+
obj.strings.insert(key.as_str().into(), from_host(v));
|
|
203
|
+
(api.drop)(v);
|
|
204
|
+
}
|
|
205
|
+
(api.drop)(k);
|
|
206
|
+
}
|
|
207
|
+
(api.drop)(keys);
|
|
208
|
+
Value::Object(VmRef::new(obj))
|
|
209
|
+
}
|
|
210
|
+
TISH_TAG_FUNCTION => host_function(h),
|
|
211
|
+
_ => Value::Null,
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/// A retained host function handle, released when the wrapping local function drops.
|
|
216
|
+
struct HostFn(TishValueRef);
|
|
217
|
+
|
|
218
|
+
// SAFETY: the handle is only dereferenced by the host through the api table, on the calling
|
|
219
|
+
// thread; the module never shares it across threads concurrently (calls are thread-affine).
|
|
220
|
+
unsafe impl Send for HostFn {}
|
|
221
|
+
unsafe impl Sync for HostFn {}
|
|
222
|
+
|
|
223
|
+
impl Drop for HostFn {
|
|
224
|
+
fn drop(&mut self) {
|
|
225
|
+
unsafe { (api().drop)(self.0) }
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
unsafe fn host_function(h: TishValueRef) -> Value {
|
|
230
|
+
let held = HostFn((api().clone)(h));
|
|
231
|
+
Value::native(move |args: &[Value]| unsafe {
|
|
232
|
+
// Borrow the whole guard: capturing only `held.0` would drop (and release) it early.
|
|
233
|
+
let held = &held;
|
|
234
|
+
let api = api();
|
|
235
|
+
let handles: Vec<TishValueRef> = args.iter().map(|a| to_host(a)).collect();
|
|
236
|
+
let r = (api.call)(held.0, handles.as_ptr(), handles.len());
|
|
237
|
+
for h in handles {
|
|
238
|
+
(api.drop)(h);
|
|
239
|
+
}
|
|
240
|
+
let thrown = (api.take_throw)();
|
|
241
|
+
if !thrown.is_null() {
|
|
242
|
+
let e = from_host(thrown);
|
|
243
|
+
(api.drop)(thrown);
|
|
244
|
+
(api.drop)(r);
|
|
245
|
+
tishlang_core::set_pending_throw(e);
|
|
246
|
+
return Value::Null;
|
|
247
|
+
}
|
|
248
|
+
let out = from_host(r);
|
|
249
|
+
(api.drop)(r);
|
|
250
|
+
out
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/// Module value → freshly owned host handle (deep copy; functions become host callables).
|
|
255
|
+
///
|
|
256
|
+
/// # Safety
|
|
257
|
+
/// [`install`] must have succeeded.
|
|
258
|
+
pub unsafe fn to_host(v: &Value) -> TishValueRef {
|
|
259
|
+
let api = api();
|
|
260
|
+
match v {
|
|
261
|
+
Value::Null => (api.new_null)(),
|
|
262
|
+
Value::Number(n) => (api.new_number)(*n),
|
|
263
|
+
Value::Bool(b) => (api.new_bool)(*b),
|
|
264
|
+
Value::String(s) => {
|
|
265
|
+
let b = s.as_bytes();
|
|
266
|
+
(api.new_string_utf8)(b.as_ptr(), b.len())
|
|
267
|
+
}
|
|
268
|
+
Value::Array(a) => {
|
|
269
|
+
let arr = (api.array_new)();
|
|
270
|
+
for e in a.borrow().iter() {
|
|
271
|
+
let h = to_host(e);
|
|
272
|
+
(api.array_push)(arr, h);
|
|
273
|
+
(api.drop)(h);
|
|
274
|
+
}
|
|
275
|
+
arr
|
|
276
|
+
}
|
|
277
|
+
Value::NumberArray(a) => {
|
|
278
|
+
let arr = (api.array_new)();
|
|
279
|
+
for e in a.borrow().to_values() {
|
|
280
|
+
let h = to_host(&e);
|
|
281
|
+
(api.array_push)(arr, h);
|
|
282
|
+
(api.drop)(h);
|
|
283
|
+
}
|
|
284
|
+
arr
|
|
285
|
+
}
|
|
286
|
+
Value::Object(o) => {
|
|
287
|
+
let obj = (api.object_new)();
|
|
288
|
+
for (k, val) in o.borrow().strings.iter() {
|
|
289
|
+
let h = to_host(val);
|
|
290
|
+
let kb = k.as_bytes();
|
|
291
|
+
(api.object_set_utf8)(obj, kb.as_ptr(), kb.len(), h);
|
|
292
|
+
(api.drop)(h);
|
|
293
|
+
}
|
|
294
|
+
obj
|
|
295
|
+
}
|
|
296
|
+
Value::Function(_) => {
|
|
297
|
+
let ctx = Box::into_raw(Box::new(v.clone())) as *mut c_void;
|
|
298
|
+
(api.new_function)(local_fn_trampoline, ctx, Some(local_fn_drop))
|
|
299
|
+
}
|
|
300
|
+
_ => (api.new_null)(),
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
extern "C" fn local_fn_trampoline(
|
|
305
|
+
ctx: *mut c_void,
|
|
306
|
+
args: *const TishValueRef,
|
|
307
|
+
argc: usize,
|
|
308
|
+
) -> TishValueRef {
|
|
309
|
+
unsafe {
|
|
310
|
+
let Value::Function(f) = &*(ctx as *const Value) else {
|
|
311
|
+
return (api().new_null)();
|
|
312
|
+
};
|
|
313
|
+
let argv: Vec<Value> = if args.is_null() {
|
|
314
|
+
Vec::new()
|
|
315
|
+
} else {
|
|
316
|
+
std::slice::from_raw_parts(args, argc)
|
|
317
|
+
.iter()
|
|
318
|
+
.map(|h| from_host(*h))
|
|
319
|
+
.collect()
|
|
320
|
+
};
|
|
321
|
+
let _ = tishlang_core::take_pending_throw();
|
|
322
|
+
let out = f.call(&argv);
|
|
323
|
+
if let Some(thrown) = tishlang_core::take_pending_throw() {
|
|
324
|
+
raise_in_host(&thrown);
|
|
325
|
+
return (api().new_null)();
|
|
326
|
+
}
|
|
327
|
+
to_host(&out)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
extern "C" fn local_fn_drop(ctx: *mut c_void) {
|
|
332
|
+
unsafe { drop(Box::from_raw(ctx as *mut Value)) }
|
|
333
|
+
}
|