@effindomv2/fui-rs 0.1.3
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/COMMERCIAL.md +7 -0
- package/Cargo.toml +34 -0
- package/LICENSE.md +9 -0
- package/README.md +88 -0
- package/package.json +54 -0
- package/scripts/build.sh +227 -0
- package/scripts/check-runtime-dependency.sh +41 -0
- package/scripts/framework-host-services.ts +40 -0
- package/scripts/generate-host-events.ts +17 -0
- package/scripts/generate-host-services.ts +28 -0
- package/scripts/hostgen/common.ts +73 -0
- package/scripts/hostgen/registry.ts +159 -0
- package/scripts/hostgen/rust-host-events.ts +171 -0
- package/scripts/hostgen/rust-host-services.ts +257 -0
- package/src/animation.rs +625 -0
- package/src/app.rs +324 -0
- package/src/assets.rs +572 -0
- package/src/bindings/mod.rs +1 -0
- package/src/bindings/ui.rs +705 -0
- package/src/bitmap.rs +317 -0
- package/src/bridge_callbacks.rs +242 -0
- package/src/context_menu_manager.rs +332 -0
- package/src/controls/anti_selection_area.rs +54 -0
- package/src/controls/button.rs +542 -0
- package/src/controls/checkbox.rs +372 -0
- package/src/controls/combobox.rs +1574 -0
- package/src/controls/context_menu.rs +1367 -0
- package/src/controls/control_template_set.rs +46 -0
- package/src/controls/control_tokens.rs +596 -0
- package/src/controls/dialog.rs +590 -0
- package/src/controls/dropdown.rs +1010 -0
- package/src/controls/form.rs +229 -0
- package/src/controls/internal/button_presenter.rs +142 -0
- package/src/controls/internal/checkbox_indicator_presenter.rs +210 -0
- package/src/controls/internal/dropdown_chevron_presenter.rs +134 -0
- package/src/controls/internal/dropdown_field_presenter.rs +269 -0
- package/src/controls/internal/dropdown_option_row_presenter.rs +184 -0
- package/src/controls/internal/mod.rs +13 -0
- package/src/controls/internal/pressable_indicator_presenter.rs +26 -0
- package/src/controls/internal/pressable_labeled_control.rs +492 -0
- package/src/controls/internal/radio_indicator_presenter.rs +208 -0
- package/src/controls/internal/selectable_popup_list.rs +597 -0
- package/src/controls/internal/slider_presenter.rs +282 -0
- package/src/controls/internal/switch_indicator_presenter.rs +235 -0
- package/src/controls/internal/text_input_core.rs +1074 -0
- package/src/controls/internal/text_input_presenter.rs +108 -0
- package/src/controls/mod.rs +235 -0
- package/src/controls/nav_link.rs +395 -0
- package/src/controls/popup.rs +191 -0
- package/src/controls/progress_bar.rs +287 -0
- package/src/controls/radio_button.rs +348 -0
- package/src/controls/radio_group.rs +283 -0
- package/src/controls/selection_area.rs +82 -0
- package/src/controls/shared.rs +68 -0
- package/src/controls/slider.rs +701 -0
- package/src/controls/switch.rs +293 -0
- package/src/controls/templating.rs +49 -0
- package/src/controls/tests.rs +3905 -0
- package/src/controls/text_area.rs +207 -0
- package/src/controls/text_input.rs +192 -0
- package/src/debug.rs +146 -0
- package/src/drag_drop.rs +424 -0
- package/src/drag_gesture.rs +258 -0
- package/src/drawing.rs +385 -0
- package/src/event.rs +1603 -0
- package/src/external_drop.rs +467 -0
- package/src/fetch.rs +500 -0
- package/src/ffi.rs +3542 -0
- package/src/file.rs +1677 -0
- package/src/focus_adorner.rs +307 -0
- package/src/focus_visibility.rs +121 -0
- package/src/frame_scheduler.rs +159 -0
- package/src/frame_signal.rs +64 -0
- package/src/generated/ffi.rs +798 -0
- package/src/generated/framework_host_services.rs +74 -0
- package/src/generated/mod.rs +2 -0
- package/src/host_services.rs +162 -0
- package/src/image_sampling.rs +78 -0
- package/src/keyboard_scroll.rs +137 -0
- package/src/keyboard_scroll_tracker.rs +385 -0
- package/src/lib.rs +547 -0
- package/src/logger.rs +56 -0
- package/src/mobile_text_selection_toolbar.rs +1034 -0
- package/src/navigation.rs +48 -0
- package/src/node/core.rs +2210 -0
- package/src/node/custom_drawable.rs +149 -0
- package/src/node/flex_box.rs +874 -0
- package/src/node/grid.rs +304 -0
- package/src/node/helpers.rs +442 -0
- package/src/node/image.rs +506 -0
- package/src/node/mod.rs +315 -0
- package/src/node/scroll_bar.rs +845 -0
- package/src/node/scroll_box.rs +514 -0
- package/src/node/scroll_state.rs +121 -0
- package/src/node/scroll_view.rs +557 -0
- package/src/node/svg_node.rs +474 -0
- package/src/node/text_node.rs +633 -0
- package/src/node/virtual_list.rs +678 -0
- package/src/panic_hook.rs +36 -0
- package/src/persisted.rs +274 -0
- package/src/platform.rs +556 -0
- package/src/popup_presenter.rs +344 -0
- package/src/selection_handle_adorner.rs +838 -0
- package/src/signal.rs +134 -0
- package/src/text.rs +1065 -0
- package/src/theme.rs +571 -0
- package/src/timers.rs +106 -0
- package/src/tool_tip.rs +167 -0
- package/src/tool_tip_manager.rs +691 -0
- package/src/transitions.rs +41 -0
- package/src/typography.rs +520 -0
- package/src/viewport.rs +108 -0
- package/src/worker.rs +308 -0
- package/src/worker_job.rs +82 -0
- package/src/worker_runtime.rs +172 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
export type HostServiceTypeName =
|
|
2
|
+
| "string"
|
|
3
|
+
| "bool"
|
|
4
|
+
| "i32"
|
|
5
|
+
| "u32"
|
|
6
|
+
| "i64"
|
|
7
|
+
| "u64"
|
|
8
|
+
| "f64"
|
|
9
|
+
| "bytes"
|
|
10
|
+
| "i32_array"
|
|
11
|
+
| "u32_array"
|
|
12
|
+
| "i64_array"
|
|
13
|
+
| "u64_array"
|
|
14
|
+
| "f64_array"
|
|
15
|
+
| "void";
|
|
16
|
+
|
|
17
|
+
interface HostServiceMethodDefinition {
|
|
18
|
+
readonly importName?: string;
|
|
19
|
+
readonly args: readonly HostServiceTypeName[];
|
|
20
|
+
readonly returns: HostServiceTypeName;
|
|
21
|
+
readonly implementation: (...args: readonly unknown[]) => unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type HostServicesDefinition = Record<string, Record<string, HostServiceMethodDefinition>>;
|
|
25
|
+
|
|
26
|
+
interface HostEventMethodDefinition {
|
|
27
|
+
readonly args: readonly HostServiceTypeName[];
|
|
28
|
+
readonly subscribe: (emit: (...args: readonly unknown[]) => void) => (() => void) | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type HostEventsDefinition = Record<string, Record<string, HostEventMethodDefinition>>;
|
|
32
|
+
|
|
33
|
+
export interface NormalizedHostServiceMethod {
|
|
34
|
+
readonly serviceName: string;
|
|
35
|
+
readonly methodName: string;
|
|
36
|
+
readonly importName: string;
|
|
37
|
+
readonly args: readonly HostServiceTypeName[];
|
|
38
|
+
readonly returns: HostServiceTypeName;
|
|
39
|
+
readonly implementation: (...args: readonly unknown[]) => unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface NormalizedHostEventMethod {
|
|
43
|
+
readonly serviceName: string;
|
|
44
|
+
readonly methodName: string;
|
|
45
|
+
readonly eventName: string;
|
|
46
|
+
readonly exportName: string;
|
|
47
|
+
readonly args: readonly HostServiceTypeName[];
|
|
48
|
+
readonly subscribe: (emit: (...args: readonly unknown[]) => void) => (() => void) | undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
52
|
+
|
|
53
|
+
function assertIdentifier(value: string, context: string): void {
|
|
54
|
+
if (!IDENTIFIER_RE.test(value)) {
|
|
55
|
+
throw new Error(`${context} "${value}" must be a valid identifier.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function capitalize(value: string): string {
|
|
60
|
+
return value.length === 0 ? value : `${value.slice(0, 1).toUpperCase()}${value.slice(1)}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function validateServiceType(type: string, context: string): asserts type is HostServiceTypeName {
|
|
64
|
+
if (
|
|
65
|
+
type === "string" ||
|
|
66
|
+
type === "bool" ||
|
|
67
|
+
type === "i32" ||
|
|
68
|
+
type === "u32" ||
|
|
69
|
+
type === "i64" ||
|
|
70
|
+
type === "u64" ||
|
|
71
|
+
type === "f64" ||
|
|
72
|
+
type === "bytes" ||
|
|
73
|
+
type === "i32_array" ||
|
|
74
|
+
type === "u32_array" ||
|
|
75
|
+
type === "i64_array" ||
|
|
76
|
+
type === "u64_array" ||
|
|
77
|
+
type === "f64_array" ||
|
|
78
|
+
type === "void"
|
|
79
|
+
) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
throw new Error(`${context} uses unsupported host-service type "${type}".`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function validateEventType(type: string, context: string): asserts type is HostServiceTypeName {
|
|
86
|
+
if (type !== "void") {
|
|
87
|
+
validateServiceType(type, context);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
throw new Error(`${context} uses unsupported host-event type "${type}".`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function listHostServiceMethods(services: HostServicesDefinition | undefined): readonly NormalizedHostServiceMethod[] {
|
|
94
|
+
if (services === undefined) {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
const methods: NormalizedHostServiceMethod[] = [];
|
|
98
|
+
const seenImports = new Set<string>();
|
|
99
|
+
for (const [serviceName, serviceMethods] of Object.entries(services)) {
|
|
100
|
+
assertIdentifier(serviceName, "Host service");
|
|
101
|
+
for (const [methodName, definition] of Object.entries(serviceMethods)) {
|
|
102
|
+
assertIdentifier(methodName, `Host service ${serviceName} method`);
|
|
103
|
+
const importName = definition.importName ?? `${serviceName}${capitalize(methodName)}`;
|
|
104
|
+
assertIdentifier(importName, `Host service ${serviceName}.${methodName} import`);
|
|
105
|
+
if (seenImports.has(importName)) {
|
|
106
|
+
throw new Error(`Duplicate host-service import name "${importName}".`);
|
|
107
|
+
}
|
|
108
|
+
seenImports.add(importName);
|
|
109
|
+
const args = [...definition.args];
|
|
110
|
+
args.forEach((type, index) => {
|
|
111
|
+
validateServiceType(type, `Host service ${serviceName}.${methodName} arg ${String(index)}`);
|
|
112
|
+
});
|
|
113
|
+
validateServiceType(definition.returns, `Host service ${serviceName}.${methodName} return`);
|
|
114
|
+
methods.push({
|
|
115
|
+
serviceName,
|
|
116
|
+
methodName,
|
|
117
|
+
importName,
|
|
118
|
+
args,
|
|
119
|
+
returns: definition.returns,
|
|
120
|
+
implementation: definition.implementation,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
methods.sort((left, right) => left.importName.localeCompare(right.importName));
|
|
125
|
+
return methods;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function listHostEventMethods(events: HostEventsDefinition | undefined): readonly NormalizedHostEventMethod[] {
|
|
129
|
+
if (events === undefined) {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
const methods: NormalizedHostEventMethod[] = [];
|
|
133
|
+
const seenEvents = new Set<string>();
|
|
134
|
+
for (const [serviceName, serviceMethods] of Object.entries(events)) {
|
|
135
|
+
assertIdentifier(serviceName, "Host event service");
|
|
136
|
+
for (const [methodName, definition] of Object.entries(serviceMethods)) {
|
|
137
|
+
assertIdentifier(methodName, `Host event ${serviceName} method`);
|
|
138
|
+
const eventName = `${serviceName}${capitalize(methodName)}`;
|
|
139
|
+
if (seenEvents.has(eventName)) {
|
|
140
|
+
throw new Error(`Duplicate host-event name "${eventName}".`);
|
|
141
|
+
}
|
|
142
|
+
seenEvents.add(eventName);
|
|
143
|
+
const args = [...definition.args];
|
|
144
|
+
args.forEach((type, index) => {
|
|
145
|
+
validateEventType(type, `Host event ${serviceName}.${methodName} arg ${String(index)}`);
|
|
146
|
+
});
|
|
147
|
+
methods.push({
|
|
148
|
+
serviceName,
|
|
149
|
+
methodName,
|
|
150
|
+
eventName,
|
|
151
|
+
exportName: `__fui_host_event_${eventName}`,
|
|
152
|
+
args,
|
|
153
|
+
subscribe: definition.subscribe,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
methods.sort((left, right) => left.eventName.localeCompare(right.eventName));
|
|
158
|
+
return methods;
|
|
159
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { listHostEventMethods, type HostServiceTypeName } from "./registry";
|
|
4
|
+
import { loadModuleExport, snakeCaseIdentifier, sourcePathForHeader } from "./common";
|
|
5
|
+
|
|
6
|
+
function rustValueType(type: HostServiceTypeName): string {
|
|
7
|
+
switch (type) {
|
|
8
|
+
case "string":
|
|
9
|
+
return "String";
|
|
10
|
+
case "bool":
|
|
11
|
+
return "bool";
|
|
12
|
+
case "i32":
|
|
13
|
+
return "i32";
|
|
14
|
+
case "u32":
|
|
15
|
+
return "u32";
|
|
16
|
+
case "i64":
|
|
17
|
+
return "i64";
|
|
18
|
+
case "u64":
|
|
19
|
+
return "u64";
|
|
20
|
+
case "f64":
|
|
21
|
+
return "f64";
|
|
22
|
+
case "bytes":
|
|
23
|
+
return "Vec<u8>";
|
|
24
|
+
case "i32_array":
|
|
25
|
+
return "Vec<i32>";
|
|
26
|
+
case "u32_array":
|
|
27
|
+
return "Vec<u32>";
|
|
28
|
+
case "i64_array":
|
|
29
|
+
return "Vec<i64>";
|
|
30
|
+
case "u64_array":
|
|
31
|
+
return "Vec<u64>";
|
|
32
|
+
case "f64_array":
|
|
33
|
+
return "Vec<f64>";
|
|
34
|
+
case "void":
|
|
35
|
+
return "()";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function callbackType(args: readonly HostServiceTypeName[]): string {
|
|
40
|
+
if (args.length === 0) {
|
|
41
|
+
return "Box<dyn Fn()>";
|
|
42
|
+
}
|
|
43
|
+
return `Box<dyn Fn(${args.map((arg) => rustValueType(arg)).join(", ")})>`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function emitExportArgs(args: readonly HostServiceTypeName[]): string {
|
|
47
|
+
const parts: string[] = [];
|
|
48
|
+
args.forEach((type, index) => {
|
|
49
|
+
if (type === "string" || type === "bytes") {
|
|
50
|
+
parts.push(`arg${String(index)}_ptr: *const u8`, `arg${String(index)}_len: u32`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (type === "i32_array" || type === "u32_array") {
|
|
54
|
+
parts.push(`arg${String(index)}_ptr: *const u32`, `arg${String(index)}_len: u32`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (type === "i64_array" || type === "u64_array" || type === "f64_array") {
|
|
58
|
+
parts.push(`arg${String(index)}_ptr: *const u64`, `arg${String(index)}_len: u32`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
parts.push(`arg${String(index)}: ${rustValueType(type)}`);
|
|
62
|
+
});
|
|
63
|
+
return parts.join(", ");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function emitDecodedArgs(args: readonly HostServiceTypeName[]): string[] {
|
|
67
|
+
const lines: string[] = [];
|
|
68
|
+
args.forEach((type, index) => {
|
|
69
|
+
if (type === "string") {
|
|
70
|
+
lines.push(
|
|
71
|
+
` let arg${String(index)} = if arg${String(index)}_ptr.is_null() || arg${String(index)}_len == 0 { String::new() } else { String::from_utf8_lossy(unsafe { std::slice::from_raw_parts(arg${String(index)}_ptr, arg${String(index)}_len as usize) }).into_owned() };`,
|
|
72
|
+
);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (type === "bytes") {
|
|
76
|
+
lines.push(
|
|
77
|
+
` let arg${String(index)} = if arg${String(index)}_ptr.is_null() || arg${String(index)}_len == 0 { Vec::new() } else { unsafe { std::slice::from_raw_parts(arg${String(index)}_ptr, arg${String(index)}_len as usize) }.to_vec() };`,
|
|
78
|
+
);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (type === "i32_array") {
|
|
82
|
+
lines.push(
|
|
83
|
+
` let arg${String(index)} = if arg${String(index)}_ptr.is_null() || arg${String(index)}_len == 0 { Vec::new() } else { unsafe { std::slice::from_raw_parts(arg${String(index)}_ptr as *const i32, arg${String(index)}_len as usize) }.to_vec() };`,
|
|
84
|
+
);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (type === "u32_array") {
|
|
88
|
+
lines.push(
|
|
89
|
+
` let arg${String(index)} = if arg${String(index)}_ptr.is_null() || arg${String(index)}_len == 0 { Vec::new() } else { unsafe { std::slice::from_raw_parts(arg${String(index)}_ptr as *const u32, arg${String(index)}_len as usize) }.to_vec() };`,
|
|
90
|
+
);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (type === "i64_array") {
|
|
94
|
+
lines.push(
|
|
95
|
+
` let arg${String(index)} = if arg${String(index)}_ptr.is_null() || arg${String(index)}_len == 0 { Vec::new() } else { unsafe { std::slice::from_raw_parts(arg${String(index)}_ptr as *const i64, arg${String(index)}_len as usize) }.to_vec() };`,
|
|
96
|
+
);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (type === "u64_array") {
|
|
100
|
+
lines.push(
|
|
101
|
+
` let arg${String(index)} = if arg${String(index)}_ptr.is_null() || arg${String(index)}_len == 0 { Vec::new() } else { unsafe { std::slice::from_raw_parts(arg${String(index)}_ptr as *const u64, arg${String(index)}_len as usize) }.to_vec() };`,
|
|
102
|
+
);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (type === "f64_array") {
|
|
106
|
+
lines.push(
|
|
107
|
+
` let arg${String(index)} = if arg${String(index)}_ptr.is_null() || arg${String(index)}_len == 0 { Vec::new() } else { unsafe { std::slice::from_raw_parts(arg${String(index)}_ptr as *const f64, arg${String(index)}_len as usize) }.to_vec() };`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
return lines;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function generateRustHostEventsFile(
|
|
115
|
+
modulePath: string,
|
|
116
|
+
exportName: string,
|
|
117
|
+
outputPath: string,
|
|
118
|
+
): Promise<void> {
|
|
119
|
+
const registry = await loadModuleExport(modulePath, exportName, "fui-rs-host-events-");
|
|
120
|
+
const methods = listHostEventMethods(registry as never);
|
|
121
|
+
const lines: string[] = [
|
|
122
|
+
`// Generated by hostgen from ${sourcePathForHeader(modulePath)}#${exportName}.`,
|
|
123
|
+
"// Do not edit by hand.",
|
|
124
|
+
"",
|
|
125
|
+
"#![allow(dead_code)]",
|
|
126
|
+
"#![allow(non_snake_case)]",
|
|
127
|
+
"",
|
|
128
|
+
"use std::cell::RefCell;",
|
|
129
|
+
"",
|
|
130
|
+
];
|
|
131
|
+
methods.forEach((method) => {
|
|
132
|
+
const handlerName = snakeCaseIdentifier(method.eventName);
|
|
133
|
+
const callbackTy = callbackType(method.args);
|
|
134
|
+
lines.push("thread_local! {");
|
|
135
|
+
lines.push(
|
|
136
|
+
` static ${handlerName.toUpperCase()}_HANDLER: RefCell<Option<${callbackTy}>> = const { RefCell::new(None) };`,
|
|
137
|
+
);
|
|
138
|
+
lines.push("}");
|
|
139
|
+
lines.push("");
|
|
140
|
+
lines.push(
|
|
141
|
+
`pub fn on_${handlerName}(callback: impl Fn(${method.args.map((arg) => rustValueType(arg)).join(", ")}) + 'static) {`,
|
|
142
|
+
);
|
|
143
|
+
lines.push(
|
|
144
|
+
` ${handlerName.toUpperCase()}_HANDLER.with(|slot| *slot.borrow_mut() = Some(Box::new(callback)));`,
|
|
145
|
+
);
|
|
146
|
+
lines.push("}");
|
|
147
|
+
lines.push("");
|
|
148
|
+
lines.push(`pub fn clear_${handlerName}() {`);
|
|
149
|
+
lines.push(` ${handlerName.toUpperCase()}_HANDLER.with(|slot| *slot.borrow_mut() = None);`);
|
|
150
|
+
lines.push("}");
|
|
151
|
+
lines.push("");
|
|
152
|
+
lines.push("#[no_mangle]");
|
|
153
|
+
lines.push(`pub extern "C" fn ${method.exportName}(${emitExportArgs(method.args)}) {`);
|
|
154
|
+
lines.push(` ${handlerName.toUpperCase()}_HANDLER.with(|slot| {`);
|
|
155
|
+
lines.push(" let handler = slot.borrow();");
|
|
156
|
+
lines.push(" let Some(callback) = handler.as_ref() else {");
|
|
157
|
+
lines.push(" return;");
|
|
158
|
+
lines.push(" };");
|
|
159
|
+
lines.push(...emitDecodedArgs(method.args));
|
|
160
|
+
if (method.args.length === 0) {
|
|
161
|
+
lines.push(" callback();");
|
|
162
|
+
} else {
|
|
163
|
+
lines.push(` callback(${method.args.map((_arg, index) => `arg${String(index)}`).join(", ")});`);
|
|
164
|
+
}
|
|
165
|
+
lines.push(" });");
|
|
166
|
+
lines.push("}");
|
|
167
|
+
lines.push("");
|
|
168
|
+
});
|
|
169
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
170
|
+
await fs.writeFile(outputPath, `${lines.join("\n")}\n`, "utf8");
|
|
171
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { listHostServiceMethods, type HostServiceTypeName } from "./registry";
|
|
4
|
+
import { loadModuleExport, snakeCaseIdentifier, sourcePathForHeader } from "./common";
|
|
5
|
+
|
|
6
|
+
type RustMode = "framework" | "app";
|
|
7
|
+
|
|
8
|
+
function rustArgType(type: HostServiceTypeName): string {
|
|
9
|
+
switch (type) {
|
|
10
|
+
case "string":
|
|
11
|
+
return "&str";
|
|
12
|
+
case "bool":
|
|
13
|
+
return "bool";
|
|
14
|
+
case "i32":
|
|
15
|
+
return "i32";
|
|
16
|
+
case "u32":
|
|
17
|
+
return "u32";
|
|
18
|
+
case "i64":
|
|
19
|
+
return "i64";
|
|
20
|
+
case "u64":
|
|
21
|
+
return "u64";
|
|
22
|
+
case "f64":
|
|
23
|
+
return "f64";
|
|
24
|
+
case "bytes":
|
|
25
|
+
return "&[u8]";
|
|
26
|
+
case "i32_array":
|
|
27
|
+
return "&[i32]";
|
|
28
|
+
case "u32_array":
|
|
29
|
+
return "&[u32]";
|
|
30
|
+
case "i64_array":
|
|
31
|
+
return "&[i64]";
|
|
32
|
+
case "u64_array":
|
|
33
|
+
return "&[u64]";
|
|
34
|
+
case "f64_array":
|
|
35
|
+
return "&[f64]";
|
|
36
|
+
case "void":
|
|
37
|
+
return "()";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function rustReturnType(type: HostServiceTypeName): string {
|
|
42
|
+
switch (type) {
|
|
43
|
+
case "string":
|
|
44
|
+
return "String";
|
|
45
|
+
case "bool":
|
|
46
|
+
return "bool";
|
|
47
|
+
case "i32":
|
|
48
|
+
return "i32";
|
|
49
|
+
case "u32":
|
|
50
|
+
return "u32";
|
|
51
|
+
case "i64":
|
|
52
|
+
return "i64";
|
|
53
|
+
case "u64":
|
|
54
|
+
return "u64";
|
|
55
|
+
case "f64":
|
|
56
|
+
return "f64";
|
|
57
|
+
case "bytes":
|
|
58
|
+
return "Vec<u8>";
|
|
59
|
+
case "i32_array":
|
|
60
|
+
return "Vec<i32>";
|
|
61
|
+
case "u32_array":
|
|
62
|
+
return "Vec<u32>";
|
|
63
|
+
case "i64_array":
|
|
64
|
+
return "Vec<i64>";
|
|
65
|
+
case "u64_array":
|
|
66
|
+
return "Vec<u64>";
|
|
67
|
+
case "f64_array":
|
|
68
|
+
return "Vec<f64>";
|
|
69
|
+
case "void":
|
|
70
|
+
return "()";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isBufferType(type: HostServiceTypeName): boolean {
|
|
75
|
+
return (
|
|
76
|
+
type === "string" ||
|
|
77
|
+
type === "bytes" ||
|
|
78
|
+
type === "i32_array" ||
|
|
79
|
+
type === "u32_array" ||
|
|
80
|
+
type === "i64_array" ||
|
|
81
|
+
type === "u64_array" ||
|
|
82
|
+
type === "f64_array"
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
function emitWasmImportArgs(args: readonly HostServiceTypeName[], returns: HostServiceTypeName): string {
|
|
88
|
+
const parts: string[] = [];
|
|
89
|
+
args.forEach((type, index) => {
|
|
90
|
+
if (type === "string" || type === "bytes") {
|
|
91
|
+
parts.push(`arg${String(index)}_ptr: *const u8`, `arg${String(index)}_len: u32`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (type === "i32_array" || type === "u32_array") {
|
|
95
|
+
parts.push(`arg${String(index)}_ptr: *const u32`, `arg${String(index)}_len: u32`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (type === "i64_array" || type === "u64_array" || type === "f64_array") {
|
|
99
|
+
parts.push(`arg${String(index)}_ptr: *const u64`, `arg${String(index)}_len: u32`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
parts.push(`arg${String(index)}: ${rustReturnType(type)}`);
|
|
103
|
+
});
|
|
104
|
+
if (isBufferType(returns)) {
|
|
105
|
+
parts.push("result_ptr: *mut u8", "result_cap: u32");
|
|
106
|
+
}
|
|
107
|
+
return parts.join(", ");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function emitWrapperArgs(args: readonly HostServiceTypeName[]): string {
|
|
111
|
+
return args.map((type, index) => `arg${String(index)}: ${rustArgType(type)}`).join(", ");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function emitWasmCallArgs(args: readonly HostServiceTypeName[]): string {
|
|
115
|
+
const parts: string[] = [];
|
|
116
|
+
args.forEach((type, index) => {
|
|
117
|
+
if (type === "string") {
|
|
118
|
+
parts.push(`arg${String(index)}.as_ptr()`, `arg${String(index)}.len() as u32`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (type === "bytes") {
|
|
122
|
+
parts.push(`arg${String(index)}.as_ptr()`, `arg${String(index)}.len() as u32`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (
|
|
126
|
+
type === "i32_array" ||
|
|
127
|
+
type === "u32_array" ||
|
|
128
|
+
type === "i64_array" ||
|
|
129
|
+
type === "u64_array" ||
|
|
130
|
+
type === "f64_array"
|
|
131
|
+
) {
|
|
132
|
+
parts.push(`arg${String(index)}.as_ptr() as *const _`, `arg${String(index)}.len() as u32`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
parts.push(`arg${String(index)}`);
|
|
136
|
+
});
|
|
137
|
+
return parts.join(", ");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function emitFrameworkNonWasmBody(importName: string, args: readonly HostServiceTypeName[]): string[] {
|
|
141
|
+
const argNames = args.map((_type, index) => `arg${String(index)}`);
|
|
142
|
+
if (importName === "fui_now_ms") {
|
|
143
|
+
return [" crate::ffi::test::host_now_ms()"];
|
|
144
|
+
}
|
|
145
|
+
return [` unsafe { crate::ffi::${importName}(${argNames.join(", ")}) }`];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function emitDecodeReturn(
|
|
149
|
+
returns: HostServiceTypeName,
|
|
150
|
+
runtimePath: string,
|
|
151
|
+
importName: string,
|
|
152
|
+
): string[] {
|
|
153
|
+
if (returns === "void") {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
if (!isBufferType(returns)) {
|
|
157
|
+
return [" raw_result"];
|
|
158
|
+
}
|
|
159
|
+
const decodeFn =
|
|
160
|
+
returns === "string" ? "decode_host_service_string_result" :
|
|
161
|
+
returns === "bytes" ? "decode_host_service_bytes_result" :
|
|
162
|
+
returns === "i32_array" ? "decode_host_service_i32_array_result" :
|
|
163
|
+
returns === "u32_array" ? "decode_host_service_u32_array_result" :
|
|
164
|
+
returns === "i64_array" ? "decode_host_service_i64_array_result" :
|
|
165
|
+
returns === "u64_array" ? "decode_host_service_u64_array_result" :
|
|
166
|
+
"decode_host_service_f64_array_result";
|
|
167
|
+
return [
|
|
168
|
+
` ${runtimePath}::${decodeFn}(result_ptr, raw_result, "${importName}")`,
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function generateRustHostServicesFile(
|
|
173
|
+
modulePath: string,
|
|
174
|
+
exportName: string,
|
|
175
|
+
outputPath: string,
|
|
176
|
+
runtimePathArg: string | undefined,
|
|
177
|
+
hostImportModule: string,
|
|
178
|
+
): Promise<void> {
|
|
179
|
+
const registry = await loadModuleExport(modulePath, exportName, "fui-rs-host-services-");
|
|
180
|
+
const methods = listHostServiceMethods(registry as never);
|
|
181
|
+
const mode: RustMode = hostImportModule === "fui_host" ? "framework" : "app";
|
|
182
|
+
const hostImportCfg = mode === "framework"
|
|
183
|
+
? 'any(target_family = "wasm", feature = "native-runtime")'
|
|
184
|
+
: 'target_family = "wasm"';
|
|
185
|
+
const fallbackCfg = mode === "framework"
|
|
186
|
+
? 'all(not(target_family = "wasm"), not(feature = "native-runtime"))'
|
|
187
|
+
: 'not(target_family = "wasm")';
|
|
188
|
+
const runtimePath =
|
|
189
|
+
runtimePathArg === undefined || runtimePathArg.length === 0
|
|
190
|
+
? mode === "framework"
|
|
191
|
+
? "crate::host_services"
|
|
192
|
+
: "fui::host_services"
|
|
193
|
+
: runtimePathArg;
|
|
194
|
+
const header = [
|
|
195
|
+
`// Generated by hostgen from ${sourcePathForHeader(modulePath)}#${exportName}.`,
|
|
196
|
+
"// Do not edit by hand.",
|
|
197
|
+
"",
|
|
198
|
+
"#![allow(dead_code)]",
|
|
199
|
+
"#![allow(non_snake_case)]",
|
|
200
|
+
"",
|
|
201
|
+
];
|
|
202
|
+
const wasmExterns: string[] = [
|
|
203
|
+
`#[cfg(${hostImportCfg})]`,
|
|
204
|
+
`#[cfg_attr(target_family = "wasm", link(wasm_import_module = "${hostImportModule}"))]`,
|
|
205
|
+
'unsafe extern "C" {',
|
|
206
|
+
];
|
|
207
|
+
for (const method of methods) {
|
|
208
|
+
const returnType = isBufferType(method.returns) ? "u32" : rustReturnType(method.returns);
|
|
209
|
+
wasmExterns.push(
|
|
210
|
+
` #[link_name = "${method.importName}"]`,
|
|
211
|
+
);
|
|
212
|
+
wasmExterns.push(
|
|
213
|
+
` fn __host_${method.importName}(${emitWasmImportArgs(method.args, method.returns)}) -> ${returnType};`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
wasmExterns.push("}");
|
|
217
|
+
const wrappers: string[] = [];
|
|
218
|
+
for (const method of methods) {
|
|
219
|
+
const wrapperName = snakeCaseIdentifier(method.importName);
|
|
220
|
+
const returnType = rustReturnType(method.returns);
|
|
221
|
+
wrappers.push(`pub fn ${wrapperName}(${emitWrapperArgs(method.args)}) -> ${returnType} {`);
|
|
222
|
+
wrappers.push(` #[cfg(${hostImportCfg})]`);
|
|
223
|
+
wrappers.push(" {");
|
|
224
|
+
if (isBufferType(method.returns)) {
|
|
225
|
+
wrappers.push(` let result_ptr = ${runtimePath}::host_service_result_buffer_ptr();`);
|
|
226
|
+
wrappers.push(` let result_cap = ${runtimePath}::host_service_result_buffer_size();`);
|
|
227
|
+
const wasmCallArgs = emitWasmCallArgs(method.args);
|
|
228
|
+
const callArgs = wasmCallArgs.length > 0 ? `${wasmCallArgs}, result_ptr, result_cap` : "result_ptr, result_cap";
|
|
229
|
+
wrappers.push(` let raw_result = unsafe { __host_${method.importName}(${callArgs}) };`);
|
|
230
|
+
wrappers.push(...emitDecodeReturn(method.returns, runtimePath, method.importName));
|
|
231
|
+
} else if (method.returns === "void") {
|
|
232
|
+
const wasmCallArgs = emitWasmCallArgs(method.args);
|
|
233
|
+
wrappers.push(` unsafe { __host_${method.importName}(${wasmCallArgs}) };`);
|
|
234
|
+
} else {
|
|
235
|
+
wrappers.push(` unsafe { __host_${method.importName}(${emitWasmCallArgs(method.args)}) }`);
|
|
236
|
+
}
|
|
237
|
+
wrappers.push(" }");
|
|
238
|
+
wrappers.push(` #[cfg(${fallbackCfg})]`);
|
|
239
|
+
wrappers.push(" {");
|
|
240
|
+
if (mode === "framework") {
|
|
241
|
+
wrappers.push(...emitFrameworkNonWasmBody(method.importName, method.args));
|
|
242
|
+
} else {
|
|
243
|
+
wrappers.push(
|
|
244
|
+
` panic!("Host service ${method.importName} is only available in wasm/browser builds.");`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
wrappers.push(" }");
|
|
248
|
+
wrappers.push("}");
|
|
249
|
+
wrappers.push("");
|
|
250
|
+
}
|
|
251
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
252
|
+
await fs.writeFile(
|
|
253
|
+
outputPath,
|
|
254
|
+
`${header.join("\n")}${wasmExterns.join("\n")}\n\n${wrappers.join("\n")}`,
|
|
255
|
+
"utf8",
|
|
256
|
+
);
|
|
257
|
+
}
|