Skip to content

Commit 22edd2c

Browse files
committed
proposal: swift turbo modules
1 parent 7b3286f commit 22edd2c

File tree

1 file changed

+185
-0
lines changed

1 file changed

+185
-0
lines changed

proposals/0000-swift-turbo-modules.md

+185
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
---
2+
title: Swift Turbo Modules
3+
author:
4+
- Oskar Kwasniewski
5+
- Riccardo Cipolleschi
6+
date: 12-08-2024
7+
---
8+
9+
# RFC0000: Swift Turbo Modules
10+
11+
## Summary
12+
13+
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.
14+
15+
## Motivation
16+
17+
The primary motivations for introducing Swift Turbo Modules are:
18+
- Enhance developer experience for iOS developers working with React Native
19+
- Allow the use of more modern language
20+
- Make barrier to entry lower
21+
22+
23+
## Detailed design
24+
25+
One of the problem why we can't adopt Swift in TurboModules is the contamination of C++ ending up in user-space.
26+
27+
### Current Situation
28+
The interfaces we generate for an Objective-C turbomodules have this shape:
29+
```objc
30+
@protocol NativeMyTurboModuleSpec <RCTBridgeModule, RCTTurboModule>
31+
@end
32+
```
33+
34+
Where the `RCTTurboModule` protocol requires the conforming object to implement a method with this signature:
35+
```objc
36+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
37+
(const facebook::react::ObjCTurboModule::InitParams &)params;
38+
```
39+
40+
As you can see, the signature of this method contains two types from C++:
41+
42+
* `std::shared_ptr<facebook::react::TurboModule>`
43+
* `facebook::react::ObjCTurboModule::InitParams`
44+
45+
### Solution
46+
47+
The idea is to wrap the `getTurboModule` invocation in a `TurboModuleWrapper` object.
48+
49+
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:
50+
```objc
51+
@interface TurboModuleWrapper: NSObject
52+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
53+
(const facebook::react::ObjCTurboModule::InitParams &)params;
54+
@end
55+
```
56+
And the base class implementation just fails, as we don't want for it to be used directly.
57+
58+
Then, the `RCTTurboModule` interface can ask each TM to actually return an implementation of the `TurboModuleWrapper` rather then the actual `TurboModule`.
59+
60+
The `TurboModuleWrapper` is a pure Objective-C class, so it will work seamlessly with ObjectiveC and Swift.
61+
62+
So, now, the public interface of a TurboModule will only have pure objc entries and no C++ code.
63+
64+
When it comes to the implementation, the user-defined TurboModule won't have to deal with any C++ code:
65+
66+
```objc
67+
@implementation MyTurboModule
68+
- (TurboModuleWrapper *)getWrapper
69+
{
70+
return [[MyTurboModuleWrapper alloc] init];
71+
}
72+
73+
// ... rest of the TM methods ...
74+
75+
@end
76+
```
77+
78+
The `MyTurboModuleWrapper` implementation can be Codegenerated!
79+
80+
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.
81+
82+
83+
The implementation will look like (note that `<UserDefinedName>` is something we get from Codegen):
84+
```objc
85+
// In the .h file
86+
87+
@interface Native<UserDefinedName>Wrapper: TurboModuleWrapper
88+
@end
89+
90+
// In the .mm file
91+
92+
@implementation Native<UserDefinedName>Wrapper
93+
94+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
95+
(const facebook::react::ObjCTurboModule::InitParams &)params;
96+
{
97+
return std::make_shared<facebook::react::Native<UserDefinedName>SpecJSI>(params);
98+
}
99+
100+
@end
101+
```
102+
103+
104+
The user-defined TurboModule has already access to this file, and so the switch from `getTurboModule` to `getWrapper` doesn't require any additional includes.
105+
106+
107+
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
108+
```objc
109+
// Step 2e: Return an exact sub-class of ObjC TurboModule
110+
std::shared_ptr<facebook::react::TurboModule> turboModule = nullptr;
111+
112+
if ([module respondsToSelector:@selector(getTurboModule:)]) {
113+
turboModule = [module getTurboModule:params];
114+
} else if ([module respondsToSelector:@selector(getWrapper)]) {
115+
auto wrapper = [module getWrapper];
116+
turboModule = [wrapper getTurboModule:params];
117+
}
118+
```
119+
120+
121+
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.
122+
123+
```swift
124+
import protocol React_Codegen.NativeSwiftTestLibrarySpec
125+
import protocol React_Codegen.TurboModuleWrapper
126+
import class React_Codegen.NativeSwiftTestLibraryWrapper
127+
128+
@objc public class SwiftTestLibrary: NSObject, NativeSwiftTestLibrarySpec {
129+
@objc public func multiply(_ a: Double, b: Double) -> NSNumber! {
130+
return a * b as NSNumber
131+
}
132+
133+
@objc public static func moduleName() -> String! {
134+
return "SwiftTestLibrary"
135+
}
136+
137+
@objc public func getWrapper() -> (any TurboModuleWrapper)! {
138+
return NativeSwiftTestLibraryWrapper()
139+
}
140+
}
141+
142+
143+
public func SwiftTestLibraryCls() -> AnyClass {
144+
return SwiftTestLibrary.self
145+
}
146+
```
147+
148+
Here is a POC implementation of the proposal: https://github.com/okwasniewski/react-native/commit/93b21d1a2e5769924ae1913e912e94296a92f3d8 (using @protocol).
149+
150+
## Drawbacks
151+
152+
- Additional complexity in codegen
153+
- Setup Swift CI/CD to test if there are no regressions breaking swift builds
154+
155+
## Alternatives
156+
157+
- Use Objective-C for all native modules
158+
159+
- **Using a protocol for the wrapper.**
160+
The pro of this is that we don't have an empty implementation for the TurboModuleWrapper object.
161+
The cons are various:
162+
* 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.
163+
* We can't codegen the default implementation for the `getTurboModule` as we won't have the base class for the protocol.
164+
* 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.
165+
166+
- **Using a custom base class for each TurboModule**
167+
The only pro of this approach is that we can remove a few lines from the definition of every TurboModule.
168+
The con of this approach are:
169+
* The C++ code remains in the public API of the TurboModule
170+
* We are creating a deeper inheritance chain which usually should be avoided.
171+
* We are asking to all the users to inherit from a different base class. This is hard to make backward compatible.
172+
173+
## Adoption strategy
174+
175+
- Introduce as an experimental feature in a future React Native release
176+
- Provide comprehensive documentation and migration guides
177+
- This proposal keeps the code backward compatible
178+
179+
## How we teach this
180+
181+
- Create detailed documentation with step-by-step guides
182+
- Update React Native's official documentation to include Swift examples
183+
- Provide sample projects demonstrating real-world use cases
184+
185+

0 commit comments

Comments
 (0)