简体   繁体   English

我可以用 Swift 编写一个 Spotlight 导入器吗?

[英]Can I Write a Spotlight Importer in Swift?

I need to write a Spotlight Importer for an application that I've written in Swift, and am referring to the official Apple guide for Writing a Spotlight Importer .我需要为我用 Swift 编写的应用程序编写一个 Spotlight Importer,我指的是 Apple 官方指南Writing a Spotlight Importer

It seems straightforward enough, however creating a Spotlight Importer project creates a default setup for an Objective-C implementation.这看起来很简单,但是创建 Spotlight Importer 项目会为 Objective-C 实现创建一个默认设置。 Now, working with Objective-C isn't a huge problem (I've used it plenty of times in the past) but everything I've written for my application is in Swift, so I'd really I'd like to write the importer in Swift too to avoid switching between languages, and also so I can share some of the code that I've already done for reading/writing files.现在,使用 Objective-C 并不是一个大问题(我过去用过很多次)但是我为我的应用程序编写的所有内容都是用 Swift 编写的,所以我真的很想写Swift 中的导入器也是为了避免在语言之间切换,这样我就可以分享一些我已经完成的用于读/写文件的代码。

Firstly, is it possible to write a Spotlight Importer using Swift instead of Objective-C?首先,是否可以使用 Swift 而不是 Objective-C 编写 Spotlight Importer? And if it is, where should I start (eg- if I take the Objective-C starting point, what would I do to switch over to Swift instead)?如果是,我应该从哪里开始(例如,如果我以 Objective-C 为起点,我该怎么做才能切换到 Swift)?

Yes , it is possible to write a Spotlight Importer entirely* in Swift!的,可以完全*用 Swift 编写一个 Spotlight Importer!

*except for a few lines of code in main.m *除了main.m中的几行代码

I've just published one here: https://github.com/foxglove/MCAPSpotlightImporter我刚刚在这里发布了一个: https ://github.com/foxglove/MCAPSpotlightImporter

Here's a detailed blog post about the implementation process: https://foxglove.dev/blog/implementing-a-macos-search-plugin-for-robotics-data这是一篇关于实施过程的详细博客文章: https ://foxglove.dev/blog/implementing-a-macos-search-plugin-for-robotics-data

The difficult part of this is implementing a plugin that's compatible with the CFPlugIn architecture.其中困难的部分是实现一个与 CFPlugIn 架构兼容的插件。 (The MDImporter-specific logic is relatively minimal.) The CFPlugIn API is based on Microsoft's COM and Apple's docs are almost 20 years old. (特定于 MDImporter 的逻辑相对最少。)CFPlugIn API 基于 Microsoft 的COM ,而Apple 的文档已有将近 20 年的历史。

The plugin is expected to be a block of memory conforming to a certain memory layout — specifically, the first value in the block must be a pointer to a virtual function table (vtable) for the requested interface (in the case of a MDImporter, this is either MDImporterInterfaceStruct or MDImporterURLInterfaceStruct ) or the base IUnknown interface.插件应该是符合特定内存布局的内存块——具体来说,块中的第一个值必须是指向所请求接口的虚函数表 (vtable) 的指针(在 MDImporter 的情况下,这是MDImporterInterfaceStructMDImporterURLInterfaceStruct )或基础 IUnknown 接口。 This layout is documented here .此布局记录在此处

I wanted to organize the Swift code into a class, but you can't control the memory layout of a Swift class instance.我想将 Swift 代码组织成一个类,但你无法控制 Swift 类实例的内存布局。 So I created a "wrapper" block of memory which holds the vtable and an unsafe pointer to the class instance.所以我创建了一个“包装器”内存块,其中包含 vtable 和指向类实例的不安全指针。 The class has a static func allocate() which uses UnsafeMutablePointer to allocate the wrapper block, create and store the class instance in it, and also initialize the vtable.该类有一个static func allocate() ,它使用 UnsafeMutablePointer 来分配包装器块,在其中创建和存储类实例,并初始化 vtable。

The vtable implements the standard COM base interface (IUnknown) functions ( QueryInterface , AddRef , and Release ) by grabbing the class instance out of the wrapper and calling the queryInterface() , addRef() , and release() methods on the instance. vtable 通过从包装器中获取类实例并调用实例上的queryInterface()addRef()release()方法来实现标准 COM 基本接口 (IUnknown) 函数( QueryInterfaceAddRefRelease )。 It also implements the Spotlight-specific ImporterImportURLData function (or ImporterImportData ).它还实现了特定于 Spotlight 的ImporterImportURLData函数(或ImporterImportData )。 Unfortunately, in my testing, it seemed like Spotlight did not pass the correct pointer to the wrapper struct as the first argument to ImporterImportURLData , so it was impossible to call a method on the class instance, so the function that actually imports attributes for a file had to be a global function .不幸的是,在我的测试中, 似乎Spotlight 没有将正确的指针传递给包装结构作为ImporterImportURLData的第一个参数,因此不可能在类实例上调用方法,因此实际导入文件属性的函数必须是一个全局函数 For this reason I wasn't able to make the plug-in implementation a more generic class that could be used with any interface — it has to be tied to a specific global importer function.出于这个原因,我无法使插件实现成为可以与任何接口一起使用的更通用的类——它必须绑定到特定的全局导入程序函数。

I'd encourage you to view the full source on GitHub , but in the interest of not being a link-only answer, here's the core functionality:我鼓励您查看GitHub 上的完整源代码,但为了不成为仅链接的答案,这里是核心功能:

final class ImporterPlugin {
  typealias VTable = MDImporterURLInterfaceStruct
  typealias Wrapper = (vtablePtr: UnsafeMutablePointer<VTable>, instance: UnsafeMutableRawPointer)
  let wrapperPtr: UnsafeMutablePointer<Wrapper>
  var refCount = 1
  let factoryUUID: CFUUID

  private init(wrapperPtr: UnsafeMutablePointer<Wrapper>, factoryUUID: CFUUID) {
    self.wrapperPtr = wrapperPtr
    self.factoryUUID = factoryUUID
    CFPlugInAddInstanceForFactory(factoryUUID)
  }

  deinit {
    let uuid = UUID(factoryUUID)
    CFPlugInRemoveInstanceForFactory(factoryUUID)
  }

  static func fromWrapper(_ plugin: UnsafeMutableRawPointer?) -> Self? {
    if let wrapper = plugin?.assumingMemoryBound(to: Wrapper.self) {
      return Unmanaged<Self>.fromOpaque(wrapper.pointee.instance).takeUnretainedValue()
    }
    return nil
  }

  func queryInterface(uuid: UUID) -> UnsafeMutablePointer<Wrapper>? {
    if uuid == kMDImporterURLInterfaceID || uuid == IUnknownUUID {
      addRef()
      return wrapperPtr
    }
    return nil
  }

  func addRef() {
    precondition(refCount > 0)
    refCount += 1
  }

  func release() {
    precondition(refCount > 0)
    refCount -= 1
    if refCount == 0 {
      Unmanaged<ImporterPlugin>.fromOpaque(wrapperPtr.pointee.instance).release()
      wrapperPtr.pointee.vtablePtr.deinitialize(count: 1)
      wrapperPtr.pointee.vtablePtr.deallocate()
      wrapperPtr.deinitialize(count: 1)
      wrapperPtr.deallocate()
    }
  }

  static func allocate(factoryUUID: CFUUID) -> Self {
    let wrapperPtr = UnsafeMutablePointer<Wrapper>.allocate(capacity: 1)
    let vtablePtr = UnsafeMutablePointer<VTable>.allocate(capacity: 1)

    let instance = Self(wrapperPtr: wrapperPtr, factoryUUID: factoryUUID)
    let unmanaged = Unmanaged.passRetained(instance)

    vtablePtr.initialize(to: VTable(
      _reserved: nil,
      QueryInterface: { wrapper, iid, outInterface in
        if let instance = ImporterPlugin.fromWrapper(wrapper) {
          if let interface = instance.queryInterface(uuid: UUID(iid)) {
            outInterface?.pointee = UnsafeMutableRawPointer(interface)
            return S_OK
          }
        }
        outInterface?.pointee = nil
        return HRESULT(bitPattern: 0x8000_0004) // E_NOINTERFACE <https://github.com/apple/swift/issues/61851>
      },
      AddRef: { wrapper in
        if let instance = ImporterPlugin.fromWrapper(wrapper) {
          instance.addRef()
        }
        return 0 // optional
      },
      Release: { wrapper in
        if let instance = ImporterPlugin.fromWrapper(wrapper) {
          instance.release()
        }
        return 0 // optional
      },
      ImporterImportURLData: { _, mutableAttributes, contentTypeUTI, url in
        // Note: in practice, the first argument `wrapper` has the wrong value passed to it, so we can't use it here
        guard let contentTypeUTI = contentTypeUTI as String?,
              let url = url as URL?,
              let mutableAttributes = mutableAttributes as NSMutableDictionary?
        else {
          return false
        }

        var attributes: [AnyHashable: Any] = mutableAttributes as NSDictionary as Dictionary
        // Call custom global function to import attributes
        let result = importAttributes(&attributes, forFileAt: url, contentTypeUTI: contentTypeUTI)
        mutableAttributes.removeAllObjects()
        mutableAttributes.addEntries(from: attributes)
        return DarwinBoolean(result)
      }
    ))
    wrapperPtr.initialize(to: (vtablePtr: vtablePtr, instance: unmanaged.toOpaque()))
    return instance
  }
}

Finally, I created an @objc class that exposes this allocate function to Obj-C, where I can call it from main.m , and return the pointer to the wrapper block from the factory function.最后,我创建了一个@objc类,它将这个allocate函数公开给 Obj-C,我可以在其中从main.m调用它,并从工厂函数返回指向包装器块的指针。 This was necessary because I didn't want to use the unstable @_cdecl attribute to expose a Swift function directly to the plug-in loader.这是必要的,因为我不想使用不稳定的@_cdecl属性将 Swift 函数直接暴露给插件加载器。

@objc public final class PluginFactory: NSObject {
  @objc public static func createPlugin(ofType type: CFUUID, factoryUUID: CFUUID) -> UnsafeMutableRawPointer? {
    if UUID(type) == kMDImporterTypeID {
      return UnsafeMutableRawPointer(ImporterPlugin.allocate(factoryUUID: factoryUUID).wrapperPtr)
    }
    return nil
  }
}
// main.m
void *MyImporterPluginFactory(CFAllocatorRef allocator, CFUUIDRef typeID) {
  return [PluginFactory createPluginOfType:typeID factoryUUID:CFUUIDCreateFromString(NULL, CFSTR("your plugin factory uuid"))];
}

See my blog post for more details.有关更多详细信息,请参阅我的博客文章

Since Apple introduced Swift as a language to be perfectly compatible with any existing Objective-C project I would suggest you just start with whatever makes things easier for you. 由于Apple将Swift作为一种语言与任何现有的Objective-C项目完美兼容,我建议您先从让事情变得更容易的事情开始。

If you know Swift best then nothing keeps you from using that – for whatever project you might want. 如果你最了解Swift那么没有什么能阻止你使用它 - 无论你想要什么样的项目。 If you want to follow a tutorial that was written for Objective-C and not updated for Swift yet, I think you have two choices (I'd personally recommend going for the second option for now): 如果你想按照为Objective-C编写的教程而不是为Swift更新,我认为你有两个选择 (我个人建议现在选择第二个选项):

  1. Write the same logic written in Objective-C within the tutorial now in Swift from scratch (nearly everything possible in Objective-C is easily possible with Swift, too). 现在在Swift中从头开始编写Objective-C中编写的相同逻辑 (几乎所有可能在Objective-C中都可以使用Swift)。 For that you need to understand the basics of Objective-C and the corresponding syntax and features in Swift though. 为此,您需要了解Objective-C的基础知识以及Swift中相应的语法和功能。

  2. Start with Objective-C to follow the tutorial and keep things easier at the beginning (no need to really understand the tutorials details). 从Objective-C开始,按照教程开始,一开始就更容易(不需要真正理解教程的细节)。 Then use the great possibility of mix and matching Swift code alongside Objective-C code to customize the code for your needs or to extend it with your own pre-existing classes. 然后使用混合和匹配Swift代码以及Objective-C代码来定制代码以满足您的需求或使用您自己的预先存在的类扩展它。

More specifically on the second option: 更具体地说,第二个选项:

If you want to write new classes just use Swift – you can perfectly use everything written in Objective-C from within Swift and vice versa. 如果你想编写新的类只需使用Swift - 你可以完全使用Swift中的Objective-C编写的所有内容,反之亦然。 If you feel you need to change classes already written in Objective-C you have these options: Extend the class written in Objective-C with a new Swift class , re-write that specific file in Swift or just edit the Objective-C file directly . 如果你觉得你需要改变已经用Objective-C类 ,您有以下选择: 扩展用Objective-C与新雨燕类的类, 再编写一个特定的文件中斯威夫特 直接 编辑 Objective-C的文件。

To learn more on how to mix and match Swift code alongside Objective-C I recommend reading Apples official documentation . 了解有关如何混合和匹配Swift代码以及Objective-C的更多信息,我建议您阅读Apples 官方文档 It's part of the free iBook "Using Swift with Cocoa and Objective-C" written by Apple engineers for developers. 它是Apple工程师为开发人员编写的免费iBook“使用Swift with Cocoa和Objective-C”的一部分。


Unfortunately Apple actually does seem to provide their template for a Spotlight Importer from within XCode for Objective-C only at the moment. 不幸的是,Apple实际上似乎确实只是在目前的XCode中为Objective-C提供了Spotlight导入器的模板。 Don't know why this is though – I can't see anything stopping them from supporting Swift. 不知道为什么会这样 - 我看不到任何阻止他们支持Swift的东西。 We should probably report this with Apples Bug Reporter to stress the fact that people are actually asking for this. 我们应该向Apples Bug Reporter报告这一点,以强调人们实际上是在要求这样做。

Hope I didn't overlook anything here, otherwise my answer will be pointless. 希望我在这里没有忽略任何东西,否则我的回答将毫无意义。 ^^ ^^


UPDATE (request) Here are some steps on where to begin to implement the first approach : 更新(请求)以下是从哪里开始实现第一种方法的一些步骤:

  • First create a Spotlight Importer project with the latest XCode version – Create a new "Cocoa Touch" class named exactly the same as your pre-created main Objective-C classes (eg "MySpotlightImporter") 首先使用最新的XCode版本创建Spotlight Importer项目 - 创建一个名为与预先创建的主要Objective-C类完全相同的新“Cocoa Touch”类(例如“MySpotlightImporter”)
  • Choose Swift and "Create Bridging Header" when asked during class creation – Re-implement the code written in the ObjC-MySpotlightImporter class within the Swift class (you might want to create a Cocoa App with Core Data support in Swift and Objective-C to get some idea of their differences) – I'm not sure if you can rewrite the GetMetaDataFile.m in Swift, too, I couldn't figure that out in my test, so you maybe need to keep it around (for now) – In case you receive any errors along the way that point to some missing configuration just search for the related files/classes in the projects "Build settings" and apply your changes there 在创建类时请选择Swift和“Create Bridging Header” - 重新实现在 Swift类中的ObjC-MySpotlightImporter类中编写的代码 (您可能希望在Swift和Objective-C中创建一个具有Core Data支持的Cocoa App了解他们的差异) - 我不确定你是否可以在Swift中重写GetMetaDataFile.m ,我无法在我的测试中找到它,所以你可能需要保持它(现在) -如果您在指向某些缺失配置的过程中收到任何错误 ,只需在项目“构建设置”中搜索相关文件/类并在那里应用您的更改

I hope this helps to get you started and is specific enough. 我希望这有助于让你开始并且足够具体。 I tried to do the necessary changes myself in order to provide an example project in Swift but unfortunately I couldn't get it working in a limited time. 我试图自己做一些必要的更改,以便在Swift中提供一个示例项目,但不幸的是我无法在有限的时间内完成它。 You may want to consider providing your code publicly though (eg on GitHub with a link posted here) in case you decide to port it yourself so others can profit from this, too. 您可能需要考虑公开提供代码(例如,在GitHub上发布此处发布的链接),以防您决定自行移植,以便其他人也能从中获益。

Good luck! 祝好运!

It took me a bit of time to get this to work. 我花了一点时间才能让它发挥作用。

Instead of adding Swift code to the mdimporter, I import an embedded framework already setup for my app. 我没有为mdimporter添加Swift代码,而是导入了已经为我的应用程序设置的嵌入式框架。

I removed all the example code except main.c and GetMetadataForFile.m . 我删除了除main.cGetMetadataForFile.m之外的所有示例代码。 In the latter I import my framework where all the functionality now resides as Swift code. 在后者中,我导入了我的框架,其中所有功能现在都作为Swift代码。

The built mdimporter is added to the app. 构建的mdimporter已添加到应用程序中。 In the File Inspector set Location to Relative to Build Products . 在“ 文件检查器”中,将“ 位置”设置为“ 相对于构建产品”

The app then adds the mdimporter with a Copy Files Build Phase. 然后,应用程序将mdimporter添加到“ 复制文件构建阶段”。

  • Destination: Wrapper 目的地: 包装
  • Subpath: Contents/Library/Spotlight 子路径: 内容/库/聚光灯

The following needs to be added to the Run Search Paths build setting, as we are linking to the app's embedded frameworks. 以下需要添加到运行搜索路径构建设置,因为我们链接到应用程序的嵌入式框架。

@loader_path/../../../../../Frameworks

If you get compiler error that the framework module can't be found when building the app, depending on how your workspace is set up, you might need to modify your app's Scheme . 如果您在构建应用程序时遇到无法找到框架模块的编译器错误,则可能需要修改应用程序的Scheme ,具体取决于工作区的设置方式。

  • Turn off Parallelize Build 关闭Parallelize Build
  • Add the Build targets in this sequence: 按以下顺序添加Build目标:
    1. Frameworks project(s) 框架项目
    2. mdimporter project mdimporter项目
    3. App project 应用项目

The additional benefit of having all the logic in a framework, is that it can be prototyped and verified in a Playground . 在框架中拥有所有逻辑的额外好处是,它可以在Playground中进行原型化和验证。 A million times easier than debugging an mdimporter plugin. 比调试mdimporter插件容易一百万倍。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM