Valdi 原生绑定(Native Bindings)完全指南:TypeScript 与原生代码的高性能双向桥接
跨平台UI组件前端移动开发【免费下载链接】ValdiValdi is a cross-platform UI framework that delivers native performance without sacrificing developer velocity.项目地址https://gitcode.com/gh_mirrors/val/Valdi点击查看免费下载导读Valdi 作为跨平台 UI 框架其核心挑战在于如何让 TypeScript 与 Objective-C / Kotlin / Java / C 之间高效、安全地通信。本文围绕 docs/docs/native-bindings.md 展开系统讲解 Valdi 的三大通信途径Context、ExportFunction、Polyglot Module、完整的类型编组Marshalling规则与限制、类型转换行为、线程模型与性能优化策略。读完本文你将掌握如何在 Valdi 组件中声明与调用原生能力、如何把复杂对象与回调安全地跨语言传递以及如何规避内存泄漏与序列化开销陷阱。Valdi 的性能设计宗旨贯穿全文我们能暴露的最高级 API 是什么既要易于且安全使用又要保证高性能。这一理念直接决定了下文所有桥接设计与类型约束。一、统一数据容器Valdi::Value是桥接的基石当数据跨越 TypeScript、Objective-C 与 Java 之间的边界时Valdi 使用一个统一的弱类型数据容器Valdi::ValueC 实现来承载所有数据从而把跨语言数据转换的开销降到最低并支撑 interned strings字符串驻留与样式styles等高级特性。从源码看Valdi::Value定义于 valdi_core/src/valdi_core/cpp/Utils/Value.hpp它是一个可容纳 64 位值/指针的弱类型容器其内部ValueType枚举完整刻画了它能承载的数据种类enum class ValueType : uint8_t { Null, Undefined, InternedString, // 驻留字符串降低重复字符串的存储与比较开销 StaticString, Int, Long, Double, Bool, Map, Array, TypedArray, Function, Error, TypedObject, // 关联 ClassSchema 的类型化对象 ProxyTypedObject, // 持有类型化对象的代理对象 ValdiObject // 任意继承 ValdiObject 的对象C/Java/Obj-C/JS 值 };在运行时一侧Valdi::Value与 JS 引擎之间的相互转换由 JavaScriptValueMarshaller.hpp 中的JavaScriptValueMarshaller负责marshall把JSValueRef按ValueSchema转换为ValueunmarshallTypedObject/unmarshallProxyObject则把类型化对象还原为 JS 值。理解这一点有助于解释后文所有类型支持与限制的根源——凡是无法被ValueType表达的类型就无法直接穿越桥接。二、通信方式一Context最常见、官方推荐对于视图的 TypeScript 代码与原生代码之间交互这一场景Context 对象是首选方案。它借助 注解annotations API让 Valdi 编译器自动生成强类型的原生接口/类替你完成 TypeScript 与原生代码之间双向的转换工作。注解之所以写在注释里是因为 TypeScript 本身不支持编译期注解——编译器TypeScriptAnnotationsManager见 compiler/compiler/Compiler/Sources/Processors/TypeScriptAnnotationsManager.swift会解析注释块中的Context、ExportModel等标记并生成对应代码。2.1 从 TypeScript 调用原生逻辑在 TS 中声明一个带ContextExportModel注解的接口并指明 iOS / Android 上生成的原生类名/** * Context * ExportModel({ * ios: SCYourComponentContext, * android: com.snap.myfeature.YourComponentContext * }) */ interface YourComponentContext { callMeFromTS?(); } /** * Component * [...] */ class YourComponent extends ComponentYourComponentViewModel, YourComponentContext { onMyButtonWasTapped() { // Calls callMeFromTS: on the SCYourComponentContext (if it has been configured) this.context.callMeFromTS?.(); } }编译器生成的 Objective-C 对等物是一个继承自SCValdiMarshallableObject的类方法被转换为可配置的 block 属性interface SCYourComponentContext: SCValdiMarshallableObject property (copy, nonatomic) SCYourComponentContextOnDoneBlock _Nullable callMeFromTS; - (instancetype _Nonnull)init; // ... end ////////// // So you can instantiate SCYourComponentContext and configure it with the callMeFromTS block: SCYourComponentContext *componentContext [[SCYourComponentContext alloc] init]; componentContext.callMeFromTS ^{ // Will be called when this.context.callMeFromTS() is called in TS. NSLog(Hello from Objective-C); }Kotlin 侧同理生成类暴露同名的函数属性package com.snap.myfeature.YourComponentContext class SCYourComponentContextImpl { val onDone: (() - Unit)? // ... } ////////// // So you can instantiate YourComponentContext and configure it with the callMeFromTS block: val componentContext YourComponentContext() componentContext.callMeFromTS { // Will be called when this.context.callMeFromTS() is called in TS. print(Hello from Kotlin) }2.2 从 JS 向原生传入回调CompletionContext 接口的方法可以携带回调参数从而把 TS 闭包传给原生侧由原生在合适时机调用TypeScriptinterface YourComponentContext { callMeFromTS?(completion: (arg: string) void); } class YourComponent extends Componentany, YourComponentContext { onMyButtonWasTapped() { this.context.callMeFromTS((arg) { console.log(the native code called the completion function with arg:, arg); }); } }Objective-CcomponentContext.callMeFromTSWithCompletion ^(YourComponentContextCallMeFromTSCompletionBlock completion) { // This will call the TS callback and provide the given value. completion(I got you loud and clear); }KotlincomponentContext.callMeFromTSWithCompletion { completion - // This will call the TS callback and provide the given value. completion(I got you loud and clear); }控制台将打印the native code called the completion function with arg: I got you loud and clear。关于字段可选性的一个重要细节详见 native-annotations.md声明为可选的字段不会在原生构造函数中初始化因此可以在后续随时赋值而不破坏已编译的原生代码非可选字段则必须在构造函数中初始化。这也是文档示例中方法/回调普遍使用?后缀的原因。三、通信方式二ExportFunction组件之外使用 TS 代码如果你需要在Valdi 组件之外例如普通的 ViewController、Activity 中调用 TypeScript 代码可以使用ExportFunction注解。它同样会生成 Objective-C / Swift / Kotlin 文件并自动处理参数的序列化与反序列化。3.1 基本用法导出一个纯函数// ExportFunction({ios: SCMultiplier, android: com.valdi.example.Multiplier}) export function multiply(left: number, right: number): number { return left * right; }iOS 调用方式先取得 JS runtime再调用生成的 C 风格函数#import ModuleName/SCMultiplier.h - (void)viewDidLoad { /// inject the idSCValdiRuntimeProtocol dependency [valdiRuntime getJSRuntimeWithBlock:^(idSCValdiJSRuntime runtime) { double result SCMultiplierMultiply(runtime, 2, 4); NSLog(Result is: %fs, result); }]; }Android 调用方式Kotlin 注入IValdiRuntime后创建 scoped JS runtimeInject lateinit var runtime: IValdiRuntime fun onCreate() { runtime.createScopedJSRuntime { val result Multiplier.multiply(it, 2, 4) println(Result is: ${result}) } }3.2 传递/返回复杂对象ExportFunction支持完整的 Valdi annotations 体系因此可以进出复杂对象用ExportModel定义数据模型用ExportModel定义带方法的接口对象再通过ExportFunction导出工厂函数// ExportModel({ios: SCValdiUser, android: com.valdi.example.User}) interface User { name: string; } // ExportModel({ios: SCValdiSearchEngine, android: com.valdi.example.SearchEngine}) interface SearchEngine { search(term: string, completion: (results: User[]) void); } // ExportFunction({ios: SCValdiSearchEngineFactory, android: com.valdi.example.SearchEngineFactory}) export function makeSearchEngine(users: User[]): SearchEngine { // Note: in a near future, you will be able to make the class itself // implements the interface and return it. const engine new ConcreteSearchEngine(users); return { search(term: string, completion: (results: User[]) void) { const results engine.performSearch(term); completion(results); } }; }iOS 使用- (void)viewDidLoad { [UIView.valdiRuntime getJSRuntimeWithBlock:^(idSCValdiJSRuntime runtime) { NSArraySCValdiUser * *allUsers fetchAllUsers(); SCValdiSearchEngine *searchEngine SCValdiSearchEngineFactoryMakeSearchEngine(runtime, allUsers); self.searchEngine searchEngine; }]; } // Later on... - (void)updateUsers { [self.searchEngine searchWithTerm:Simon withCompletion:^(NSArraySCValdiUser * *results) { NSLog(Found those users: %, results); }]; }Android 使用Inject lateinit var runtime: IValdiRuntime var searchEngine: SearchEngine? null fun onCreate() { runtime.createScopedJSRuntime { val allUsers fetchAllUsers() val searchEngine SearchEngineFactory.makeSearchEngine(it, allUsers) this.searchEngine searchEngine } } // Later on... fun updateUsers() { searchEngine?.search(Simon) { println(Found those users: ${it}) } }线程语义调用 JS 函数时如果必要会自动异步派发到 JS 线程。如果你的函数需要同步向平台代码返回值务必在createScopedJSRuntime作用域内调用而使用异步 completion 函数则永远不会遇到这个问题。四、通信方式三Polyglot Module可复用原生模块如果你希望用 Kotlin、Java、Objective-C、C 或 Swift 编写一个可复用的独立模块同时对外暴露 TS API可以创建polyglot module。这是三种方式中模块化程度最高、适合作为独立库分发的形态。详见 polyglot modules 文档。三种通信方式的选型速查通信方式适用场景生成物Context组件View的 TS 逻辑与原生交互最常见强类型原生接口/类block/lambda 属性ExportFunction组件之外调用 TS 函数可直接调用的 Objective-C 函数 / Kotlin 函数Polyglot Module用其他语言编写可复用模块并暴露 TS API独立模块 生成的桥接代码五、类型系统参考本节完整列出可在 TypeScript 与原生代码之间**编组marshall**的全部类型。核心判断标准是该类型是否能被Valdi::Value与ValueSchema表达参见 Value.hpp 与 JavaScriptValueMarshaller.hpp 的marshall/unmarshall双向转换。5.1 支持的原始类型TypeScript TypeiOS (Obj-C)iOS (Swift)Android (Kotlin)CNotesstringNSString *StringStringstd::stringUTF-8 encodednumberdoubleDoubleDoubledouble64-bit floating pointbooleanBOOLBoolBooleanboolvoidvoidVoidUnitvoidFor return typesanyidAnyAnyValueDynamic/untyped valueobjectidAnyAnyValueUntyped object注意number在原生侧恒为double64 位浮点不存在隐式的 int 转换详见下文数字精度。5.2 支持的复杂类型TypeScript TypeNative EquivalentNotesT[]Array/NSArray/ListArrays of any supported typePromiseTAsync operationMapped to platform async patternsCancelablePromiseTCancelable asyncExtended Promise with cancellationMapstring, anyDictionary/MapCurrently only supports string keys and any values(arg: T) RBlock/Closure/LambdaFunction/callback typesT \| null \| undefinedOptional/nullableNullable typesT?Optional/nullableTypeScript optional syntax5.3 特殊类型TypeScript TypePurposeNative EquivalentUint8ArrayBinary dataData(iOS) /ByteArray(Android)Long64-bit integersint64_t/LongNumberExplicit doubleSame asnumberbut explicit5.4 生成类型Generated Types通过注解定义的类型可在 TS 与原生之间传递ExportModelinterfaces/classes → 生成原生类数据模型/可双向传递的对象ExportProxyinterfaces → 原生必须自行实现ExportEnumenums → 生成原生枚举示例// ExportModel({ios: SCUser, android: com.example.User}) interface User { name: string; age: number; friends?: User[]; // Optional array of User }5.5 泛型类型Generic Types生成类型支持泛型但仅适用于带ExportModel注解的类型且泛型参数必须在接口边界解析为具体类型// ExportModel({ios: SCContainer, android: com.example.Container}) interface ContainerT { value: T; } // Usage interface MyContext { userContainer: ContainerUser; stringContainer: Containerstring; }Note:Generics only work on types annotated withExportModel. Generic parameters must be resolved to concrete types at the interface boundary.六、类型限制与约束6.1 联合类型Union Types✅ 支持与null/undefined的联合。property: string | null; property: string | undefined; property?: string; // Equivalent to: string | undefined❌ 不支持其他任何联合类型包括字符串字面量联合。property: string | number; // ERROR: Only null/undefined unions supported property: red | blue | green; // ERROR: Use enum instead解决方案多值选择一律使用ExportEnum枚举// ExportEnum({ios: Color, android: com.example.Color}) enum Color { Red red, Blue blue, Green green } interface Config { color: Color; // ✅ Works }6.2 Map 类型的部分支持✅ 支持Mapstring, any字符串键 any 值。⚠️ 部分支持Mapstring, User这类类型化 Map会被降级视为Mapstring, any。❌ 不支持非字符串键如Mapnumber, string。Workaround需要类型化字典时用带ExportModel的接口 索引签名// ExportModel interface UserMap { [key: string]: User; // Use index signature }6.3 数组嵌套数组可任意深度嵌套matrix: number[][]; // ✅ 2D array cube: number[][][]; // ✅ 3D array6.4 接口继承Interface InheritanceExportModel/ExportProxy接口可以extends另一个 TypeScript 接口有两种模式控制生成的原生类中呈现哪些成员。Flatten 模式默认父类成员被合并进子类的原生类。/** * ExportModel({ios: SCTimestamped, android: com.example.Timestamped}) */ export interface Timestamped { createdAt: number; updatedAt: number; } /** * ExportModel({ios: SCUser, android: com.example.User}) */ export interface User extends Timestamped { id: string; name: string; } // Generated native User exposes: createdAt, updatedAt, id, name.Flatten 模式对父接口有严格前置要求必须有注解普通export interface Foo无注解会被编译器从 TypeScript companion 的 dump 中过滤掉flatten 会以指名类型和文件的明确错误失败。必须在同一模块每个 Valdi 模块是隔离编译的flattener 的父类查找索引只包含当前编译中的文件。跨模块继承如Child extends ParentFromDependency是 v1 限制此类场景请使用ignoreInheritance: true直至跨模块父类解析落地。父类成员不得自带注解若继承的成员本身携带Untyped、WorkerThread、Injectable、ConstructorOmitted、SingleCall、AllowSyncCall、UntypedMap等注解flatten 会拒绝并报错——这些语义无法在 v1 中针对子类的文件偏移安全地重新解释。此时应使用ignoreInheritance: true或移除父类成员上的注解。多级继承链按深度优先遍历祖父类成员在前、父类其次、子类最后。菱形继承子类同时继承 B 和 C而 B、C 都继承 A中 A 的成员只会被合并一次。TS-only 模式ignoreInheritance: true父类仅用于 TypeScript 类型推导原生类只从子类自身主体生成。/** * ExportModel({ignoreInheritance: true}) */ export interface GenerateThumbnailError extends StepErrorTypeGenerateThumbnailErrorCode { code: GenerateThumbnailErrorCode; message?: string; nonFatal?: boolean; } // Generated native class: code, message?, nonFatal?. Parent generics ignored.何时选择ignoreInheritance: true父类是泛型ParentT、无注解、位于.d.ts文件或你想完全掌控原生字段列表。该模式下编译器跳过所有 flatten 守卫。❌ 不支持仅针对 flatten 模式——需要这些能力请用ignoreInheritanceinterface Child extends ParentT {} // Generic parents — v1 limitation. interface Child implements Foo {} // implements clauses. interface A extends B {} interface B extends A {} // Cycles → compiler error.冲突策略Collision policy若子类在 flatten 模式下重声明了继承的成员名编译器默认报错。inheritanceCollisionPolicy注解参数为未来覆盖模式childWins、sameTypeOnly预留目前仅支持error。七、类型转换行为7.1 数字精度TypeScriptnumber恒定映射为原生double64 位浮点interface Data { count: number; // Native: double (not int!) }需要精确的 64 位整数时使用Long类型interface Data { timestamp: Long; // Use Long type for precise 64-bit integers }Warning:JavaScript numbers can only precisely represent integers up to 2^53 - 1. For larger integers, useLongtype or string representation.7.2 数组转换数组在跨越原生/TS 边界时是整体复制的interface DataProcessor { // Array is copied from native to TS processData(items: string[]): void; // Array is copied from TS to native getData(): string[]; }影响一端的修改不会影响另一端大数组存在序列化开销二进制数据建议用Uint8Array以降低开销。7.3 对象编组Object Marshalling只有带注解的对象才能被编组// ✅ Can be passed - has ExportModel // ExportModel interface User { name: string; } // ❌ Cannot be passed - plain object interface Config { data: { key: string, value: number }; // ERROR }解决方案一使用Untypedany动态对象无类型安全interface Config { // Untyped data: any; // Dynamic object, no type safety }解决方案二定义正规接口// ExportModel interface KeyValue { key: string; value: number; } interface Config { data: KeyValue; // ✅ Works }7.4 回调生命周期回调采用引用计数管理interface Context { callback: () void; }内存管理规则原生侧持有回调的强引用原生对象销毁时释放回调使用SingleCall可在首次调用后自动释放。interface Context { // SingleCall onComplete: () void; // Automatically released after first call }这一点与 native-annotations.md 中关于 retain cycle 的警告互为印证根视图对应的 TS 组件会在 Objective-C 根视图实例 dealloc 时被销毁因此务必避免视图模型/组件上下文依赖之间形成循环引用否则根视图可能泄漏、TS 组件无法销毁。八、性能考量8.1 线程安全所有 TypeScript 函数调用都会自动派发到 JavaScript 线程interface DataFetcher { // Automatically dispatches to JS thread fetchData(): PromiseData; }需要同步调用时必须显式进入 scoped runtime// iOS: Use scoped runtime [valdiRuntime getJSRuntimeWithBlock:^(idSCValdiJSRuntime runtime) { // Synchronous calls work here double result MyFunction(runtime, arg); }];// Android: Use scoped runtime runtime.createScopedJSRuntime { jsRuntime - // Synchronous calls work here val result MyFunction.call(jsRuntime, arg) }重活放到工作线程用WorkerThread注解把大数据处理从 JS 线程挪走。interface Processor { // WorkerThread processLargeDataset(data: Uint8Array): PromiseResult; }8.2 数据序列化开销每个参数穿越边界时都会经历一次序列化/反序列化对应JavaScriptValueMarshaller的marshall/unmarshall流程。因此要批量化减少穿越次数// ❌ BAD: Many small calls for (let i 0; i 1000; i) { context.updateProgress(i); // 1000 boundary crossings! } // ✅ GOOD: Batch operations context.updateProgressBatch(progressArray); // 1 boundary crossing8.3 回调开销尽量设计低通话量chatty的 API// ❌ BAD: Callback for each item interface ItemProcessor { processItems(items: Item[], onEachItem: (item: Item) void): void; } // ✅ GOOD: Single completion callback interface ItemProcessor { processItems(items: Item[]): PromiseItem[]; }九、最佳实践Dos ✅Keep interfaces simple- Use primitive types when possible优先使用原始类型UseExportModelfor complex types- Dont rely onanyDocument null behavior- Be explicit about optional propertiesUseSingleCallfor one-time callbacks- Prevents memory leaks防止内存泄漏Batch operations- Reduce boundary crossings减少边界穿越次数UsePromisefor async operations- More ergonomic than callbacksUseUint8Arrayfor binary data- More efficient than arraysDonts ❌Dont use union types(except with null/undefined)Dont pass large objects frequently- High serialization costDont assume synchronous execution- Use scoped runtime if neededDont hold strong references to callbacks indefinitelyDont pass plain JavaScript objects- UseExportModelinsteadDont useMapwith non-string keys常见模式异步操作首选 Promiseinterface DataFetcher { // ✅ PREFERRED: Promise-based fetchData(): PromiseData; }双向通信原生 → TS 回调 TS → 原生方法interface Chat { sendMessage(text: string): void; // Native → TS callbacks onMessageReceived?: (text: string) void; onError?: (error: string) void; }资源管理务必提供清理方法interface FileReader { open(path: string): void; read(): Uint8Array; close(): void; // Important: Always provide cleanup }十、进一步阅读Native Annotations - 完整注解参考ExportModel/ExportProxy/ExportEnum/Context等全部注解与参数Native Context - Context 模式与依赖注入Polyglot Modules - 用原生语言编写可复用模块Integration Codelabs - 原生集成逐步指南源码级延伸阅读想深入桥接层实现可查看 valdi_core/src/valdi_core/cpp/Utils/Value.hppValdi::Value容器与ValueType枚举、valdi/src/valdi/runtime/JavaScript/JavaScriptValueMarshaller.hppJS ↔ Value 编组器以及 compiler/compiler/Compiler/Sources/Processors/TypeScriptAnnotationsManager.swift注解解析与校验逻辑。赞分享跨平台UI组件前端移动开发【免费下载链接】ValdiValdi is a cross-platform UI framework that delivers native performance without sacrificing developer velocity.项目地址https://gitcode.com/gh_mirrors/val/Valdi点击查看免费下载相关推荐Valdi Native Annotations 实战用 TypeScript 注解把业务逻辑桥接到 iOS 与 Android 原生代码Valdi Native Annotations 实战用 TypeScript 注解把业务逻辑桥接到 iOS 与 Android 原生代码 在 Valdi 中跨平台UI组件前端移动开发Valdi Component 完全指南TypeScript 组件、原生元素与生命周期详解Valdi Component 完全指南TypeScript 组件、原生元素与生命周期详解 Valdi 是一个跨平台 UI 框架其所有界面都构建在统一的 C跨平台UI组件前端移动开发DORA 与 ROS2 桥接实战YAML 动态桥与原生代码 API 双通道指南DORA 与 ROS2 桥接实战YAML 动态桥与原生代码 API 双通道指南 导读 本文以 DORA 仓库中 examples/ros2 bridge h机器人人工智能ROS消息路由创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考