From 22edd2c7853aaf2ee654b57f4521e43d668103fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Mon, 12 Aug 2024 09:56:21 +0200 Subject: [PATCH 1/2] proposal: swift turbo modules --- proposals/0000-swift-turbo-modules.md | 185 ++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 proposals/0000-swift-turbo-modules.md diff --git a/proposals/0000-swift-turbo-modules.md b/proposals/0000-swift-turbo-modules.md new file mode 100644 index 00000000..ec4cf990 --- /dev/null +++ b/proposals/0000-swift-turbo-modules.md @@ -0,0 +1,185 @@ +--- +title: Swift Turbo Modules +author: +- Oskar Kwasniewski +- Riccardo Cipolleschi +date: 12-08-2024 +--- + +# RFC0000: Swift Turbo Modules + +## Summary + +This RFC aims to allow developers to write Turbo Modules using Swift. This will allow the usage of more modern language making maintenance of native modules easier and more accessible. + +## Motivation + +The primary motivations for introducing Swift Turbo Modules are: +- Enhance developer experience for iOS developers working with React Native +- Allow the use of more modern language +- Make barrier to entry lower + + +## Detailed design + +One of the problem why we can't adopt Swift in TurboModules is the contamination of C++ ending up in user-space. + +### Current Situation +The interfaces we generate for an Objective-C turbomodules have this shape: +```objc +@protocol NativeMyTurboModuleSpec +@end +``` + +Where the `RCTTurboModule` protocol requires the conforming object to implement a method with this signature: +```objc + - (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params; +``` + +As you can see, the signature of this method contains two types from C++: + +* `std::shared_ptr` +* `facebook::react::ObjCTurboModule::InitParams` + +### Solution + +The idea is to wrap the `getTurboModule` invocation in a `TurboModuleWrapper` object. + +The `TurboModuleWrapper` object is a base class that is supposed to be extended by a companion object for TurboModules. The base class has this interface: +```objc +@interface TurboModuleWrapper: NSObject +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params; +@end +``` +And the base class implementation just fails, as we don't want for it to be used directly. + +Then, the `RCTTurboModule` interface can ask each TM to actually return an implementation of the `TurboModuleWrapper` rather then the actual `TurboModule`. + +The `TurboModuleWrapper` is a pure Objective-C class, so it will work seamlessly with ObjectiveC and Swift. + +So, now, the public interface of a TurboModule will only have pure objc entries and no C++ code. + +When it comes to the implementation, the user-defined TurboModule won't have to deal with any C++ code: + +```objc +@implementation MyTurboModule +- (TurboModuleWrapper *)getWrapper +{ +return [[MyTurboModuleWrapper alloc] init]; +} + +// ... rest of the TM methods ... + +@end +``` + +The `MyTurboModuleWrapper` implementation can be Codegenerated! + +We don't even need to ask our users to write that code themselves, as we have all the informations we need in the Codegen already. + + +The implementation will look like (note that `` is something we get from Codegen): +```objc +// In the .h file + +@interface NativeWrapper: TurboModuleWrapper +@end + +// In the .mm file + +@implementation NativeWrapper + +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params; +{ + return std::make_sharedSpecJSI>(params); +} + +@end +``` + + +The user-defined TurboModule has already access to this file, and so the switch from `getTurboModule` to `getWrapper` doesn't require any additional includes. + + +Finally, we will have to update the `RCTTurboModuleManager` to take this new object into consideration. So we have to modify the provide Turbomodule with the following code +```objc + // Step 2e: Return an exact sub-class of ObjC TurboModule + std::shared_ptr turboModule = nullptr; + + if ([module respondsToSelector:@selector(getTurboModule:)]) { + turboModule = [module getTurboModule:params]; + } else if ([module respondsToSelector:@selector(getWrapper)]) { + auto wrapper = [module getWrapper]; + turboModule = [wrapper getTurboModule:params]; + } +``` + + +Additionally we need to make sure that `React_Codegen` module is compatible with importing to Swift. I did a small test and adding few ifdefs to React_Codegen headers allows us to use Swift. + +```swift +import protocol React_Codegen.NativeSwiftTestLibrarySpec +import protocol React_Codegen.TurboModuleWrapper +import class React_Codegen.NativeSwiftTestLibraryWrapper + +@objc public class SwiftTestLibrary: NSObject, NativeSwiftTestLibrarySpec { + @objc public func multiply(_ a: Double, b: Double) -> NSNumber! { + return a * b as NSNumber + } + + @objc public static func moduleName() -> String! { + return "SwiftTestLibrary" + } + + @objc public func getWrapper() -> (any TurboModuleWrapper)! { + return NativeSwiftTestLibraryWrapper() + } +} + + +public func SwiftTestLibraryCls() -> AnyClass { + return SwiftTestLibrary.self +} +``` + +Here is a POC implementation of the proposal: https://github.com/okwasniewski/react-native/commit/93b21d1a2e5769924ae1913e912e94296a92f3d8 (using @protocol). + +## Drawbacks + +- Additional complexity in codegen +- Setup Swift CI/CD to test if there are no regressions breaking swift builds + +## Alternatives + +- Use Objective-C for all native modules + +- **Using a protocol for the wrapper.** + The pro of this is that we don't have an empty implementation for the TurboModuleWrapper object. + The cons are various: + * The `TurboModule` itself can't adopt the `TurboModuleWrapper` protocol as, otherwise, the C++ signature will come back to the public API of the I don't think this will work. + * We can't codegen the default implementation for the `getTurboModule` as we won't have the base class for the protocol. + * We could use a protocol and create a companion object in the codegen which extends the protocol and it is returned by the TurboModule. This solution works, but adds a bit of ceremonies to the base implementation above. + +- **Using a custom base class for each TurboModule** + The only pro of this approach is that we can remove a few lines from the definition of every TurboModule. + The con of this approach are: + * The C++ code remains in the public API of the TurboModule + * We are creating a deeper inheritance chain which usually should be avoided. + * We are asking to all the users to inherit from a different base class. This is hard to make backward compatible. + +## Adoption strategy + +- Introduce as an experimental feature in a future React Native release +- Provide comprehensive documentation and migration guides +- This proposal keeps the code backward compatible + +## How we teach this + +- Create detailed documentation with step-by-step guides +- Update React Native's official documentation to include Swift examples +- Provide sample projects demonstrating real-world use cases + + From a30cce22e23ed018caa4f7e7ace63d0853bc8f8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwa=C5=9Bniewski?= Date: Mon, 12 Aug 2024 12:07:47 +0200 Subject: [PATCH 2/2] Apply suggestions from code review Co-authored-by: Riccardo Cipolleschi --- proposals/0000-swift-turbo-modules.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/proposals/0000-swift-turbo-modules.md b/proposals/0000-swift-turbo-modules.md index ec4cf990..723647b2 100644 --- a/proposals/0000-swift-turbo-modules.md +++ b/proposals/0000-swift-turbo-modules.md @@ -17,12 +17,12 @@ This RFC aims to allow developers to write Turbo Modules using Swift. This will The primary motivations for introducing Swift Turbo Modules are: - Enhance developer experience for iOS developers working with React Native - Allow the use of more modern language -- Make barrier to entry lower +- Lower the entry barrier to write a native turbo module ## Detailed design -One of the problem why we can't adopt Swift in TurboModules is the contamination of C++ ending up in user-space. +One of the reasons why we can't adopt Swift in TurboModules is the contamination of C++ types ending up in user-space. ### Current Situation The interfaces we generate for an Objective-C turbomodules have this shape: @@ -31,20 +31,20 @@ The interfaces we generate for an Objective-C turbomodules have this shape: @end ``` -Where the `RCTTurboModule` protocol requires the conforming object to implement a method with this signature: +The `RCTTurboModule` protocol requires the conforming object to implement a method with this signature: ```objc - (std::shared_ptr)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params; ``` -As you can see, the signature of this method contains two types from C++: +The signature of this method contains two types from C++: * `std::shared_ptr` * `facebook::react::ObjCTurboModule::InitParams` ### Solution -The idea is to wrap the `getTurboModule` invocation in a `TurboModuleWrapper` object. +The idea is to wrap the `getTurboModule` invocation in a `ModuleFactory` object. The `TurboModuleWrapper` object is a base class that is supposed to be extended by a companion object for TurboModules. The base class has this interface: ```objc @@ -65,7 +65,7 @@ When it comes to the implementation, the user-defined TurboModule won't have to ```objc @implementation MyTurboModule -- (TurboModuleWrapper *)getWrapper +- (TurboModuleWrapper *)moduleFactory { return [[MyTurboModuleWrapper alloc] init]; } @@ -118,12 +118,12 @@ Finally, we will have to update the `RCTTurboModuleManager` to take this new obj ``` -Additionally we need to make sure that `React_Codegen` module is compatible with importing to Swift. I did a small test and adding few ifdefs to React_Codegen headers allows us to use Swift. +Additionally we need to make sure that `ReactCodegen` module is compatible with importing to Swift. I did a small test and adding few ifdefs to React_Codegen headers allows us to use Swift. ```swift -import protocol React_Codegen.NativeSwiftTestLibrarySpec -import protocol React_Codegen.TurboModuleWrapper -import class React_Codegen.NativeSwiftTestLibraryWrapper +import protocol ReactCodegen.NativeSwiftTestLibrarySpec +import protocol ReactCodegen.TurboModuleWrapper +import class ReactCodegen.NativeSwiftTestLibraryWrapper @objc public class SwiftTestLibrary: NSObject, NativeSwiftTestLibrarySpec { @objc public func multiply(_ a: Double, b: Double) -> NSNumber! {