Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class DependencyProviderRegistrationSerializer: Serializer {
func serialize() -> String {
let factoryName = provider.isEmptyDependency ? "factoryEmptyDependencyProvider" : factoryFuncNameSerializer.serialize()
return """
registerProviderFactory("\(provider.unprocessed.pathString)", \(factoryName))\n
registerProviderFactory(\(provider.unprocessed.pathHash), \(factoryName))\n
"""
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@ class OutputSerializer: Serializer {
}

// MARK: - Registration
private func registerProviderFactory(_ componentPath: String, _ factory: @escaping (NeedleFoundation.Scope) -> AnyObject) {
__DependencyProviderRegistry.instance.registerDependencyProviderFactory(for: componentPath, factory)
private func registerProviderFactory(_ pathHash: Int, _ factory: @escaping (NeedleFoundation.Scope) -> AnyObject) {
__DependencyProviderRegistry.instance.registerDependencyProviderFactory(forPathHash: pathHash, factory)
}

#if !NEEDLE_DYNAMIC
Expand Down
13 changes: 13 additions & 0 deletions Generator/Sources/NeedleFramework/Models/DependencyProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,19 @@ struct DependencyProvider {
}
.joined(separator: "->")
}

/// Stable FNV-1a hash of the path string. This must match the hash
/// computed by `StableFNVHasher` at runtime.
var pathHash: Int {
let offsetBasis: UInt64 = 0xcbf29ce484222325
let prime: UInt64 = 0x100000001b3
var hash = offsetBasis
for byte in pathString.utf8 {
hash ^= UInt64(byte)
hash &*= prime
}
return Int(bitPattern: UInt(hash))
}
}

/// The data model representing a dependency provider to be generated for a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,22 +50,22 @@ class DependencyGraphExporterTests: AbstractGeneratorTests {
XCTAssertTrue(generated.contains("import UIKit"))
XCTAssertTrue(generated.contains("// MARK: - Registration"))
XCTAssertTrue(generated.contains("""
registerProviderFactory(\"^->RootComponent->LoggedInComponent->GameComponent\", factorycf9c02c4def4e3d508816cd03d3cf415b70dfb0e)
registerProviderFactory(-322842588535013842, factorycf9c02c4def4e3d508816cd03d3cf415b70dfb0e)
"""))
XCTAssertTrue(generated.contains("""
registerProviderFactory(\"^->RootComponent->LoggedInComponent->GameComponent->ScoreSheetComponent\", factory3f7d60e2119708f293bac0d8c882e1e0d9b5eda1)
registerProviderFactory(3176341359983622439, factory3f7d60e2119708f293bac0d8c882e1e0d9b5eda1)
"""))
XCTAssertTrue(generated.contains("""
registerProviderFactory(\"^->RootComponent->LoggedInComponent->ScoreSheetComponent\", factory3f7d60e2119708f293ba0b20504d5a9e5588d7b3)
registerProviderFactory(-8216646763020093181, factory3f7d60e2119708f293ba0b20504d5a9e5588d7b3)
"""))
XCTAssertTrue(generated.contains("""
registerProviderFactory(\"^->RootComponent->LoggedOutComponent\", factory1434ff4463106e5c4f1bb3a8f24c1d289f2c0f2e)
registerProviderFactory(6062161635131552075, factory1434ff4463106e5c4f1bb3a8f24c1d289f2c0f2e)
"""))
XCTAssertTrue(generated.contains("""
registerProviderFactory(\"^->RootComponent->LoggedInComponent\", factory2d08e87342cecea575b3e3b0c44298fc1c149afb)
registerProviderFactory(-860435685645118366, factory2d08e87342cecea575b3e3b0c44298fc1c149afb)
"""))
XCTAssertTrue(generated.contains("""
registerProviderFactory(\"^->RootComponent\", factoryEmptyDependencyProvider)
registerProviderFactory(-3171806194175578407, factoryEmptyDependencyProvider)
"""))
XCTAssertTrue(generated.contains("// MARK: - Traversal Helpers"))
XCTAssertTrue(generated.contains("""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class DependencyProviderSerializerTaskTests: AbstractGeneratorTests {
switch provider.unprocessed.pathString {
case "^->RootComponent->LoggedInComponent->GameComponent":
XCTAssertEqual(serializedProviders[1].registration, """
registerProviderFactory("^->RootComponent->LoggedInComponent->GameComponent", factorycf9c02c4def4e3d508816cd03d3cf415b70dfb0e)
registerProviderFactory(-322842588535013842, factorycf9c02c4def4e3d508816cd03d3cf415b70dfb0e)

""")
XCTAssertEqual(serializedProviders[1].content, """
Expand All @@ -52,7 +52,7 @@ private func factorycf9c02c4def4e3d508816cd03d3cf415b70dfb0e(_ component: Needle
""")
case "^->RootComponent->LoggedInComponent->GameComponent->ScoreSheetComponent":
XCTAssertEqual(serializedProviders[1].registration, """
registerProviderFactory("^->RootComponent->LoggedInComponent->GameComponent->ScoreSheetComponent", factory3f7d60e2119708f293bac0d8c882e1e0d9b5eda1)
registerProviderFactory(3176341359983622439, factory3f7d60e2119708f293bac0d8c882e1e0d9b5eda1)

""")
XCTAssertEqual(serializedProviders[1].content, """
Expand All @@ -64,7 +64,7 @@ private func factory3f7d60e2119708f293bac0d8c882e1e0d9b5eda1(_ component: Needle
""")
case "^->RootComponent->LoggedInComponent->ScoreSheetComponent":
XCTAssertEqual(serializedProviders[1].registration, """
registerProviderFactory("^->RootComponent->LoggedInComponent->ScoreSheetComponent", factory62cd15b035cb1b1ab3e00b20504d5a9e5588d7b3)
registerProviderFactory(-8216646763020093181, factory62cd15b035cb1b1ab3e00b20504d5a9e5588d7b3)

""")
XCTAssertEqual(serializedProviders[1].content, """
Expand All @@ -76,7 +76,7 @@ private func factory62cd15b035cb1b1ab3e00b20504d5a9e5588d7b3(_ component: Needle
""")
case "^->RootComponent->LoggedOutComponent":
XCTAssertEqual(serializedProviders[1].registration, """
registerProviderFactory("^->RootComponent->LoggedOutComponent", factory1434ff4463106e5c4f1bb3a8f24c1d289f2c0f2e)
registerProviderFactory(6062161635131552075, factory1434ff4463106e5c4f1bb3a8f24c1d289f2c0f2e)

""")
XCTAssertEqual(serializedProviders[1].content, """
Expand All @@ -88,12 +88,12 @@ private func factory1434ff4463106e5c4f1bb3a8f24c1d289f2c0f2e(_ component: Needle
""")
case "^->RootComponent->LoggedInComponent":
XCTAssertEqual(serializedProviders[1].registration, """
registerProviderFactory("^->RootComponent->LoggedInComponent", factory2d08e87342cecea575b3e3b0c44298fc1c149afb)
registerProviderFactory(-860435685645118366, factory2d08e87342cecea575b3e3b0c44298fc1c149afb)

""")
case "^->RootComponent":
XCTAssertEqual(serializedProviders[0].registration, """
registerProviderFactory("^->RootComponent", factoryEmptyDependencyProvider)
registerProviderFactory(-3171806194175578407, factoryEmptyDependencyProvider)

""")
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,28 +52,28 @@ class PluginizedDependencyGraphExporterTests: AbstractPluginizedGeneratorTests {
XCTAssertTrue(generated.contains("private let needleDependenciesHash : String? = \"86deb40d0ec1c9fc9fd5e5e8fc17a167\""))
XCTAssertTrue(generated.contains("// MARK: - Registration"))
XCTAssertTrue(generated.contains("""
registerProviderFactory(\"^->RootComponent->LoggedOutComponent\", factory1434ff4463106e5c4f1bb3a8f24c1d289f2c0f2e)
registerProviderFactory(6062161635131552075, factory1434ff4463106e5c4f1bb3a8f24c1d289f2c0f2e)
"""))
XCTAssertTrue(generated.contains("""
registerProviderFactory("^->RootComponent", factoryEmptyDependencyProvider)
registerProviderFactory(-3171806194175578407, factoryEmptyDependencyProvider)
"""))
XCTAssertTrue(generated.contains("""
registerProviderFactory(\"^->RootComponent->LoggedInComponent->GameComponent->GameNonCoreComponent->ScoreSheetComponent\", factoryb11b7d1dec7e3c9b3dca49b41e44e0ed6a6f8eaf)
registerProviderFactory(7090139472367530245, factoryb11b7d1dec7e3c9b3dca49b41e44e0ed6a6f8eaf)
"""))
XCTAssertTrue(generated.contains("""
registerProviderFactory(\"^->RootComponent->LoggedInComponent->LoggedInNonCoreComponent->ScoreSheetComponent\", factory3306c50e89e2421d0b0c65d055996113f3c13de1)
registerProviderFactory(-6719425751305065620, factory3306c50e89e2421d0b0c65d055996113f3c13de1)
"""))
XCTAssertTrue(generated.contains("""
registerProviderFactory(\"^->RootComponent->LoggedInComponent->GameComponent->GameNonCoreComponent\", factoryEmptyDependencyProvider)
registerProviderFactory(5898069836772964084, factoryEmptyDependencyProvider)
"""))
XCTAssertTrue(generated.contains("""
registerProviderFactory(\"^->RootComponent->LoggedInComponent->LoggedInNonCoreComponent\", factoryEmptyDependencyProvider)
registerProviderFactory(-7553811199512117245, factoryEmptyDependencyProvider)
"""))
XCTAssertTrue(generated.contains("""
registerProviderFactory(\"^->RootComponent->LoggedInComponent->GameComponent\", factorycf9c02c4def4e3d508816cd03d3cf415b70dfb0e)
registerProviderFactory(-322842588535013842, factorycf9c02c4def4e3d508816cd03d3cf415b70dfb0e)
"""))
XCTAssertTrue(generated.contains("""
registerProviderFactory(\"^->RootComponent->LoggedInComponent\", factoryEmptyDependencyProvider)
registerProviderFactory(-860435685645118366, factoryEmptyDependencyProvider)
"""))
XCTAssertTrue(generated.contains("""
__PluginExtensionProviderRegistry.instance.registerPluginExtensionProviderFactory(for: \"GameComponent\") { component in
Expand Down
8 changes: 8 additions & 0 deletions NeedleFoundation.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
/* End PBXAggregateTarget section */

/* Begin PBXBuildFile section */
OBJ_89 /* StableFNVHasher.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_90 /* StableFNVHasher.swift */; };
OBJ_91 /* StableFNVHasherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_92 /* StableFNVHasherTests.swift */; };
OBJ_42 /* Bootstrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* Bootstrap.swift */; };
OBJ_43 /* Component.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* Component.swift */; };
OBJ_44 /* DependencyProviderRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_12 /* DependencyProviderRegistry.swift */; };
Expand Down Expand Up @@ -78,6 +80,8 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
OBJ_90 /* StableFNVHasher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StableFNVHasher.swift; sourceTree = "<group>"; };
OBJ_92 /* StableFNVHasherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StableFNVHasherTests.swift; sourceTree = "<group>"; };
"NeedleFoundation::NeedleFoundation::Product" /* NeedleFoundation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = NeedleFoundation.framework; sourceTree = BUILT_PRODUCTS_DIR; };
"NeedleFoundation::NeedleFoundationTest::Product" /* NeedleFoundationTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = NeedleFoundationTest.framework; sourceTree = BUILT_PRODUCTS_DIR; };
"NeedleFoundation::NeedleFoundationTestTests::Product" /* NeedleFoundationTestTests.xctest */ = {isa = PBXFileReference; lastKnownFileType = file; path = NeedleFoundationTestTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -138,6 +142,7 @@
isa = PBXGroup;
children = (
OBJ_12 /* DependencyProviderRegistry.swift */,
OBJ_90 /* StableFNVHasher.swift */,
);
path = Internal;
sourceTree = "<group>";
Expand Down Expand Up @@ -185,6 +190,7 @@
OBJ_23 /* ComponentTests.swift */,
OBJ_24 /* DependencyProviderRegistryTests.swift */,
OBJ_25 /* Pluginized */,
OBJ_92 /* StableFNVHasherTests.swift */,
);
name = NeedleFoundationTests;
path = Tests/NeedleFoundationTests;
Expand Down Expand Up @@ -372,6 +378,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 0;
files = (
OBJ_89 /* StableFNVHasher.swift in Sources */,
OBJ_42 /* Bootstrap.swift in Sources */,
OBJ_43 /* Component.swift in Sources */,
OBJ_44 /* DependencyProviderRegistry.swift in Sources */,
Expand Down Expand Up @@ -413,6 +420,7 @@
OBJ_83 /* ComponentTests.swift in Sources */,
OBJ_84 /* DependencyProviderRegistryTests.swift in Sources */,
OBJ_85 /* PluginizedComponentTests.swift in Sources */,
OBJ_91 /* StableFNVHasherTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
38 changes: 29 additions & 9 deletions Sources/NeedleFoundation/Internal/DependencyProviderRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,23 @@ public class __DependencyProviderRegistry {
/// The singleton instance.
public static let instance = __DependencyProviderRegistry()

/// Register the given factory closure with a pre-computed path hash.
///
/// - note: This method is thread-safe.
/// - parameter pathHash: The stable hash of the component path,
/// computed by the Needle code generator.
/// - parameter dependencyProviderFactory: The closure that takes in a
/// component to be injected and returns a provider instance that conforms
/// to the component's dependency protocol.
public func registerDependencyProviderFactory(forPathHash pathHash: Int, _ dependencyProviderFactory: @escaping (Scope) -> AnyObject) {
providerFactoryLock.lock()
defer {
providerFactoryLock.unlock()
}

providerFactories[pathHash] = dependencyProviderFactory
}

/// Register the given factory closure with given key.
///
/// - note: This method is thread-safe.
Expand All @@ -45,7 +62,8 @@ public class __DependencyProviderRegistry {
providerFactoryLock.unlock()
}

providerFactories[componentPath.hashValue] = dependencyProviderFactory
let key = StableFNVHasher.hash(componentPath)
providerFactories[key] = dependencyProviderFactory
}

/// Unregister the given factory closure with given key.
Expand All @@ -58,9 +76,10 @@ public class __DependencyProviderRegistry {
defer {
providerFactoryLock.unlock()
}
providerFactories.removeValue(forKey: componentPath.hashValue)
let key = StableFNVHasher.hash(componentPath)
providerFactories.removeValue(forKey: key)
}

/// Retrieve the dependency provider for the given componentpath.
///
/// - parameter componentpath: The component path that uses the returned dependency provider.
Expand All @@ -70,12 +89,13 @@ public class __DependencyProviderRegistry {
defer {
providerFactoryLock.unlock()
}

return providerFactories[componentPath.hashValue]

let key = StableFNVHasher.hash(componentPath)
return providerFactories[key]
}

// MARK: - Internal

/// Retrieve the dependency provider for the given component and its parent.
///
/// - parameter component: The component that uses the returned dependency provider.
Expand All @@ -86,8 +106,8 @@ public class __DependencyProviderRegistry {
providerFactoryLock.unlock()
}

let pathString = component.path.joined(separator: "->")
if let factory = providerFactories[pathString.hashValue] {
let key = StableFNVHasher.hash(component.path, separator: "->")
if let factory = providerFactories[key] {
return factory(component)
} else {
// This case should never occur with properly generated Needle code.
Expand Down
59 changes: 59 additions & 0 deletions Sources/NeedleFoundation/Internal/StableFNVHasher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//
// Copyright (c) 2026. Uber Technologies
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

/// A deterministic hasher using FNV-1a (64-bit). Unlike Swift's `Hasher`,
/// this produces the same output across processes and platforms, so hashes
/// can be pre-computed at build time by the Needle code generator.
struct StableFNVHasher {
private static let offsetBasis: UInt64 = 0xcbf29ce484222325
private static let prime: UInt64 = 0x100000001b3

private var hash: UInt64 = offsetBasis

/// Hash a single string.
static func hash(_ string: String) -> Int {
var hasher = StableFNVHasher()
hasher.combine(string)
return hasher.finalize()
}

/// Hash an array of strings as if they were joined by `separator`.
/// Produces the same result as `hash(segments.joined(separator: separator))`
/// without allocating the joined string.
static func hash(_ segments: [String], separator: String) -> Int {
var hasher = StableFNVHasher()
var first = true
for segment in segments {
if !first {
hasher.combine(separator)
}
first = false
hasher.combine(segment)
}
return hasher.finalize()
}

private mutating func combine(_ string: String) {
for byte in string.utf8 {
hash ^= UInt64(byte)
hash &*= Self.prime
}
}

private func finalize() -> Int {
Int(bitPattern: UInt(hash))
}
}
16 changes: 16 additions & 0 deletions Tests/NeedleFoundationTests/DependencyProviderRegistryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,22 @@ class DependencyProviderRegistryTests: XCTestCase {
XCTAssertTrue(expectedProvider === actualProvider)
XCTAssertTrue(appComponent.rootComponent.dependency === expectedProvider)
}

func test_registerProviderFactoryForPathHash_verifyRetrievingProvider() {
let expectedProvider = MockRootDependencyProvider()

let path = ["^", "MockAppComponent", "MockRootComponent"]
let pathHash = StableFNVHasher.hash(path, separator: "->")
__DependencyProviderRegistry.instance.registerDependencyProviderFactory(forPathHash: pathHash) { (component: Scope) -> AnyObject in
return expectedProvider
}

let appComponent = MockAppComponent()
let actualProvider = __DependencyProviderRegistry.instance.dependencyProvider(for: appComponent.rootComponent)

XCTAssertTrue(expectedProvider === actualProvider)
XCTAssertTrue(appComponent.rootComponent.dependency === expectedProvider)
}
}

class MockAppComponent: BootstrapComponent {
Expand Down
Loading
Loading