Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Create React Native ViewManager (+ Props + State) for HybridViews #500

Merged
merged 29 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c6ba220
fix: Fix required prop connecting
mrousavy Jan 15, 2025
19b6b9a
fix: Use default value and register View!
mrousavy Jan 15, 2025
2410f70
feat: Add types to `getHostComponent`
mrousavy Jan 15, 2025
1b66e79
feat: Expose `getView()` and call it!
mrousavy Jan 15, 2025
8de491c
fix: Pass to C++ as `void*`
mrousavy Jan 15, 2025
ed79295
fix: Fix name and get view
mrousavy Jan 15, 2025
38e4fe5
perf: Use `&` for `getSwiftPart()`
mrousavy Jan 15, 2025
5bd8734
add comments
mrousavy Jan 15, 2025
3a2ea44
feat: Add `CachedProp.hpp`
mrousavy Jan 15, 2025
224925f
fix: Fix OwningRef/BorrowingRef empty copy constructo
mrousavy Jan 15, 2025
8fa40b2
fix: Properly downcast
mrousavy Jan 16, 2025
53ec8b4
perf: Sort view props alphabetically because fabric is implemented th…
mrousavy Jan 16, 2025
d030add
fix: Check for null in OwningReference
mrousavy Jan 16, 2025
ccfc707
format
mrousavy Jan 16, 2025
eb5cf6b
fix: Rename
mrousavy Jan 16, 2025
6daa311
fix: Dont sort alphabetically
mrousavy Jan 16, 2025
1e962bd
feat: Add `try`/`catch` block for prop parsing
mrousavy Jan 16, 2025
a9427ae
fix: Merge props if `nullptr`
mrousavy Jan 16, 2025
d3b9e54
fix: Fix imports
mrousavy Jan 16, 2025
8772df9
feat: Add `beforeUpdate()` and `afterUpdate()`
mrousavy Jan 16, 2025
8e56d0b
feat: Generate view config as .json
mrousavy Jan 16, 2025
8828679
feat: Generate Kotlin/JNI code for this
mrousavy Jan 20, 2025
b73562a
fix: getState() call
mrousavy Jan 20, 2025
d974f76
fix: Use JNI descriptor separator (/)
mrousavy Jan 20, 2025
441a032
fix: Use views/ import
mrousavy Jan 20, 2025
deae283
fix: Add Android extra impl
mrousavy Jan 20, 2025
322fba8
fix: Use `override` instead of `virtual` destructor
mrousavy Jan 20, 2025
a8b075d
feat: Add if-guard to wrap `registerNative` calls
mrousavy Jan 20, 2025
10d208a
fix: Move `#if` check up
mrousavy Jan 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions docs/docs/nitrogen.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,13 @@ All the generated sources (`nitrogen/generated/`) need to be part of your librar

#### iOS

On iOS, you need to call `add_nitrogen_files(...)` from your library's `.podspec`. Put this **after** any `s.source_files =` calls:
On iOS, you need to call `add_nitrogen_files(...)` from your library's `.podspec`. Put this at the very end of your spec declaration:
```ruby
load 'nitrogen/generated/ios/NitroExample+autolinking.rb'
add_nitrogen_files(s)
Pod::Spec.new do |s|
# ...
load 'nitrogen/generated/ios/NitroExample+autolinking.rb'
add_nitrogen_files(s)
end
```

#### Android
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,17 @@ export function createHybridObjectIntializer(): SourceFile[] {
const autolinkingClassName = `${NitroConfig.getAndroidCxxLibName()}OnLoad`

const jniRegistrations = getJNINativeRegistrations()
.map((r) => `${r.namespace}::${r.className}::registerNatives();`)
.map((r) => {
const code = `${r.namespace}::${r.className}::registerNatives();`
if (r.ifGuard != null) {
return `
#if ${r.ifGuard}
${code}
#endif`.trim()
} else {
return code
}
})
.filter(isNotDuplicate)

const autolinkedHybridObjects = NitroConfig.getAutolinkedHybridObjects()
Expand Down
2 changes: 1 addition & 1 deletion packages/nitrogen/src/syntax/c++/CppHybridObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ namespace ${cxxNamespace} {
explicit ${name.HybridTSpec}(): HybridObject(TAG) { }

// Destructor
virtual ~${name.HybridTSpec}() { }
~${name.HybridTSpec}() override = default;

public:
// Properties
Expand Down
2 changes: 1 addition & 1 deletion packages/nitrogen/src/syntax/kotlin/FbjniHybridObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ ${spaces} public virtual ${name.HybridTSpec} {
_javaPart(jni::make_global(jThis)) {}

public:
virtual ~${name.JHybridTSpec}() {
~${name.JHybridTSpec}() override {
// Hermes GC can destroy JS objects on a non-JNI Thread.
jni::ThreadScope::WithClassLoader([&] { _javaPart.reset(); });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface JNINativeRegistration {
namespace: string
className: string
import: SourceImport
ifGuard?: string
}

const jniNativeRegistrations: JNINativeRegistration[] = []
Expand Down
2 changes: 1 addition & 1 deletion packages/nitrogen/src/syntax/swift/SwiftCxxTypeHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ void* _Nonnull get_${name}(${name} cppType) {
throw std::runtime_error("Class \\"${HybridTSpec}\\" is not implemented in Swift!");
}
#endif
${swiftPartType} swiftPart = swiftWrapper->getSwiftPart();
${swiftPartType}& swiftPart = swiftWrapper->getSwiftPart();
return swiftPart.toUnsafe();
}
`.trim(),
Expand Down
41 changes: 32 additions & 9 deletions packages/nitrogen/src/syntax/swift/SwiftHybridObjectBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,40 @@ export function createSwiftHybridObjectCxxBridge(
const name = getHybridObjectName(spec.name)
const moduleName = NitroConfig.getIosModuleName()

const propertiesBridge = spec.properties
.map((p) => getPropertyForwardImplementation(p))
.join('\n\n')
const methodsBridge = spec.methods
.map((m) => getMethodForwardImplementation(m))
.join('\n\n')
const propertiesBridge = spec.properties.map((p) =>
getPropertyForwardImplementation(p)
)

const methodsBridge = spec.methods.map((m) =>
getMethodForwardImplementation(m)
)

const baseClasses = spec.baseTypes.map((base) => {
const baseName = getHybridObjectName(base.name)
return baseName.HybridTSpecCxx
})
const hasBase = baseClasses.length > 0

if (spec.isHybridView && !hasBase) {
methodsBridge.push(
`
public final func getView() -> UnsafeMutableRawPointer {
return Unmanaged.passRetained(__implementation.view).toOpaque()
}
`.trim(),
`
public final func beforeUpdate() {
__implementation.beforeUpdate()
}
`.trim(),
`
public final func afterUpdate() {
__implementation.afterUpdate()
}
`.trim()
)
}

const hybridObject = new HybridObjectType(spec)
const bridgedType = new SwiftCxxBridgedType(hybridObject)
const bridge = bridgedType.getRequiredBridge()
Expand Down Expand Up @@ -183,10 +204,10 @@ ${hasBase ? `public class ${name.HybridTSpecCxx} : ${baseClasses.join(', ')}` :
}

// Properties
${indent(propertiesBridge, ' ')}
${indent(propertiesBridge.join('\n\n'), ' ')}

// Methods
${indent(methodsBridge, ' ')}
${indent(methodsBridge.join('\n\n'), ' ')}
}
`

Expand Down Expand Up @@ -337,7 +358,9 @@ namespace ${cxxNamespace} {

public:
// Get the Swift part
inline ${iosModuleName}::${name.HybridTSpecCxx} getSwiftPart() noexcept { return _swiftPart; }
inline ${iosModuleName}::${name.HybridTSpecCxx}& getSwiftPart() noexcept {
return _swiftPart;
}

public:
// Get memory pressure
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,18 @@ interface SwiftHybridObjectRegistration {
requiredImports: SourceImport[]
}

export function getHybridObjectConstructorCall(
hybridObjectName: string
): string {
const swiftNamespace = NitroConfig.getIosModuleName()
const autolinkingClassName = `${NitroConfig.getIosModuleName()}Autolinking`
return `${swiftNamespace}::${autolinkingClassName}::create${hybridObjectName}();`
}

export function createSwiftHybridObjectRegistration({
hybridObjectName,
swiftClassName,
}: Props): SwiftHybridObjectRegistration {
const autolinkingClassName = `${NitroConfig.getIosModuleName()}Autolinking`
const swiftNamespace = NitroConfig.getIosModuleName()
const { HybridTSpecCxx, HybridTSpecSwift, HybridTSpec } =
getHybridObjectName(hybridObjectName)

Expand Down Expand Up @@ -55,7 +61,7 @@ public static func create${hybridObjectName}() -> ${bridge.getTypeCode('swift')}
HybridObjectRegistry::registerHybridObjectConstructor(
"${hybridObjectName}",
[]() -> std::shared_ptr<HybridObject> {
${type.getCode('c++')} hybridObject = ${swiftNamespace}::${autolinkingClassName}::create${hybridObjectName}();
${type.getCode('c++')} hybridObject = ${getHybridObjectConstructorCall(hybridObjectName)}
return hybridObject;
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { createFileMetadataString, escapeCppName } from '../syntax/helpers.js'
import { NitroConfig } from '../config/NitroConfig.js'
import { getHybridObjectName } from '../syntax/getHybridObjectName.js'
import { includeHeader } from '../syntax/c++/includeNitroHeader.js'
import { createHostComponentJs } from './createHostComponentJs.js'

interface ViewComponentNames {
propsClassName: `${string}Props`
Expand Down Expand Up @@ -40,7 +41,7 @@ export function createViewComponentShadowNodeFiles(
)
}

const name = getHybridObjectName(spec.name)
const { T, HybridT } = getHybridObjectName(spec.name)
const {
propsClassName,
stateClassName,
Expand All @@ -53,7 +54,7 @@ export function createViewComponentShadowNodeFiles(
const namespace = NitroConfig.getCxxNamespace('c++', 'views')

const properties = spec.properties.map(
(p) => `${p.type.getCode('c++')} ${escapeCppName(p.name)};`
(p) => `CachedProp<${p.type.getCode('c++')}> ${escapeCppName(p.name)};`
)
const cases = spec.properties.map(
(p) => `case hashString("${p.name}"): return true;`
Expand All @@ -74,6 +75,7 @@ ${createFileMetadataString(`${component}.hpp`)}
#include <optional>
#include <NitroModules/NitroDefines.hpp>
#include <NitroModules/NitroHash.hpp>
#include <NitroModules/CachedProp.hpp>
#include <react/renderer/core/ConcreteComponentDescriptor.h>
#include <react/renderer/core/PropsParserContext.h>
#include <react/renderer/components/view/ConcreteViewShadowNode.h>
Expand All @@ -88,14 +90,15 @@ namespace ${namespace} {
/**
* The name of the actual native View.
*/
extern const char ${nameVariable}[] = "${name.HybridT}";
extern const char ${nameVariable}[] = "${T}";

/**
* Props for the "${spec.name}" View.
*/
class ${propsClassName} final: public react::ViewProps {
public:
explicit ${propsClassName}() = default;
${propsClassName}() = default;
${propsClassName}(const ${propsClassName}&);
${propsClassName}(const react::PropsParserContext& context,
${createIndentation(propsClassName.length)} const ${propsClassName}& sourceProps,
${createIndentation(propsClassName.length)} const react::RawProps& rawProps);
Expand All @@ -112,23 +115,34 @@ namespace ${namespace} {
*/
class ${stateClassName} final {
public:
explicit ${stateClassName}() = default;
${stateClassName}() = default;

public:
void setProps(const ${propsClassName}& props) { _props = props; }
void setProps(${propsClassName}&& props) { _props.emplace(props); }
const std::optional<${propsClassName}>& getProps() const { return _props; }

public:
#ifdef ANDROID
${stateClassName}(const CustomStateData& previousState, folly::dynamic data) {}
folly::dynamic getDynamic() const {
throw std::runtime_error("${stateClassName} does not support folly!");
}
react::MapBuffer getMapBuffer() const {
throw std::runtime_error("${stateClassName} does not support MapBuffer!");
};
#endif

private:
std::optional<${propsClassName}> _props;
};

/**
* The Shadow Node for the "${spec.name}" View.
*/
using ${shadowNodeClassName} = react::ConcreteViewShadowNode<${nameVariable},
${shadowIndent} react::ViewEventEmitter,
${shadowIndent} ${propsClassName},
${shadowIndent} ${stateClassName}>;
using ${shadowNodeClassName} = react::ConcreteViewShadowNode<${nameVariable} /* "${HybridT}" */,
${shadowIndent} ${propsClassName} /* custom props */,
${shadowIndent} react::ViewEventEmitter /* default */,
${shadowIndent} ${stateClassName} /* custom state */>;

/**
* The Component Descriptor for the "${spec.name}" View.
Expand All @@ -146,43 +160,59 @@ namespace ${namespace} {
} // namespace ${namespace}

#else
#warning "View Component '${name.HybridT}' will be unavailable in React Native, because it requires React Native 78 or higher."
#warning "View Component '${HybridT}' will be unavailable in React Native, because it requires React Native 78 or higher."
#endif
`.trim()

// .cpp code
const propInitializers = [
'react::ViewProps(context, sourceProps, rawProps, filterObjectKeys)',
]
const propCopyInitializers = ['react::ViewProps()']
for (const prop of spec.properties) {
const name = escapeCppName(prop.name)
const type = prop.type.getCode('c++')
propInitializers.push(
`
/* ${prop.name} */ ${escapeCppName(prop.name)}([&](){
const react::RawValue* rawValue = rawProps.at("${prop.name}", nullptr, nullptr);
if (rawValue == nullptr) { ${prop.type.kind === 'optional' ? 'return nullptr;' : `throw std::runtime_error("${spec.name}: Prop \\"${prop.name}\\" is required and cannot be undefined!");`} }
const auto& [runtime, value] = (std::pair<jsi::Runtime*, const jsi::Value&>)*rawValue;
return JSIConverter<${prop.type.getCode('c++')}>::fromJSI(*runtime, value);
${name}([&]() -> CachedProp<${type}> {
try {
const react::RawValue* rawValue = rawProps.at("${prop.name}", nullptr, nullptr);
if (rawValue == nullptr) return sourceProps.${name};
const auto& [runtime, value] = (std::pair<jsi::Runtime*, const jsi::Value&>)*rawValue;
return CachedProp<${type}>::fromRawValue(*runtime, value, sourceProps.${name});
} catch (const std::exception& exc) {
throw std::runtime_error(std::string("${spec.name}.${prop.name}: ") + exc.what());
}
}())`.trim()
)
propCopyInitializers.push(`${name}(other.${name})`)
}

const ctorIndent = createIndentation(propsClassName.length * 2)
const componentCode = `
${createFileMetadataString(`${component}.cpp`)}

#if REACT_NATIVE_VERSION >= 78

#include "${component}.hpp"
#include <string>
#include <exception>
#include <utility>
#include <NitroModules/JSIConverter.hpp>

#if REACT_NATIVE_VERSION >= 78
#include <react/renderer/core/RawValue.h>
#include <react/renderer/core/ShadowNode.h>
#include <react/renderer/core/ComponentDescriptor.h>
#include <react/renderer/components/view/ViewProps.h>

namespace ${namespace} {

${propsClassName}::${propsClassName}(const react::PropsParserContext& context,
${ctorIndent} const ${propsClassName}& sourceProps,
${ctorIndent} const react::RawProps& rawProps):
${indent(propInitializers.join(',\n'), ' ')} {
// TODO: Instead of eagerly converting each prop, only convert the ones that changed on demand.
}
${indent(propInitializers.join(',\n'), ' ')} { }

${propsClassName}::${propsClassName}(const ${propsClassName}& other):
${indent(propCopyInitializers.join(',\n'), ' ')} { }

bool ${propsClassName}::filterObjectKeys(const std::string& propName) {
switch (hashString(propName)) {
Expand Down Expand Up @@ -214,7 +244,7 @@ namespace ${namespace} {
#endif
`.trim()

return [
const files: SourceFile[] = [
{
name: `${component}.hpp`,
content: componentHeaderCode,
Expand All @@ -230,4 +260,7 @@ namespace ${namespace} {
subdirectory: ['views'],
},
]
const jsFiles = createHostComponentJs(spec)
files.push(...(jsFiles as unknown as SourceFile[]))
return files
}
32 changes: 32 additions & 0 deletions packages/nitrogen/src/views/createHostComponentJs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Language } from '../getPlatformSpecs.js'
import type { HybridObjectSpec } from '../syntax/HybridObjectSpec.js'
import type { SourceFile } from '../syntax/SourceFile.js'
import { getHybridObjectName } from '../syntax/getHybridObjectName.js'
import { indent } from '../utils.js'

export function createHostComponentJs(spec: HybridObjectSpec): SourceFile[] {
const { T } = getHybridObjectName(spec.name)
const props = spec.properties.map((p) => `"${p.name}": true`)

const code = `
{
"uiViewClassName": "${T}",
"supportsRawText": false,
"bubblingEventTypes": {},
"directEventTypes": {},
"validAttributes": {
${indent(props.join(',\n'), ' ')}
}
}
`.trim()

return [
{
content: code,
language: 'json' as Language,
name: `${T}Config.json`,
platform: 'shared',
subdirectory: [],
},
]
}
Loading
Loading