客观-铁锈

2020-08-29 05:41:27

这将是另一个帖子,我做了一些可笑的事情,然后告诉你们我是如何做到的,所以让我们直接开始吧。

使用objc_rust::*;use std::ffi::cstr;pub fn main(){#[link(name=";Foundation";,Kind=";framework";)]extern{}objc!{let cls=ObjCClass::Lookup(";NSNumber\0";)。UnWrap();let value=[[cls.。Into()number WithUnsignedInt:42 u32]string Value];let result=unsafe{cstr::from_ptr([value UTF8String])};println!(";string:{}";,result。To_string_lossy();//string:42}}。

是的,这是嵌入Objective-C语法的Rust代码,它可以工作。你为什么要做这样的事?也许您希望在iOS应用程序的Rust和Objective-C部分之间实现更紧密的互操作。也许你想完全用Rust编写你的iOS应用程序。或者,也许你只是想看看在你的同事随意说了一句话之后,这是否可能。

这篇帖子将详细介绍实现这一点所需的一切,所以这里有一个目录表:

第3-5节实际上可能对其他想要编写Rust宏的人有用,所以即使这个项目只是一个玩具,你仍然可以从阅读这篇文章中得到一些东西。但是,如果您希望在生产中使用铁锈公司的Objective-C,您不应该在这里使用我的不安全玩具。取而代之的是,使用史蒂芬·谢尔顿的Objc板条箱。Sheldon在项目开始时还有一篇博客文章,除了简单的消息发送实现之外,还谈到了他的设计过程。

也就是说,如果您想要查看我的小怪物的全部源代码,您可以查看存储库。

这些天来,我的普通观众可能是SWIFT开发人员,但我预计这一次也会与一些Rust人员进行交流。这两组人都很容易没有太多直接使用Objective-C的经验,Objective-C自2001年和2007年发布以来,一直是苹果在MacOS1和iOS上使用的主要语言。所以,这里有一个简单的总结:除了开始使用“Object”类型之外,它“只”是C。您对这些对象所做的几乎所有操作都基于在运行时中实现的动态调度模型。由于这句话的两个部分(“动态调度”和“在运行时实现”),人们想出了许多聪明而强大的技术来使程序更简单、更具表现力或更具可扩展性,尽管有时会以安全性、保密性和稳定性为代价。

然而,与我们更相关的是,只要您没有真正严格的性能约束,运行时库中(几乎)所有功能都可用的语言是一种易于动态桥接的语言。事情是这样的:您在Objective-C中所做的几乎所有事情都是通过发送消息来调用方法,其工作方式是方法是存储在调度表中的常规C函数,该调度表以称为选择器的唯一字符串为关键字。Apple库中优化程度最高的代码很可能是objc_msgSend,它接受接收器、选择器和方法的参数,在接收器的类调度表中(缓存)查找选择器,然后直接跳转到适当的、多态选择的方法实现。

Objective-C运行时公开了更多内容,但消息绝对是最重要的。

…。哦,还有一件事。因为Objective-C是C的扩展,所以它必须使用与C语法不冲突的语法。这意味着前面有@符号的关键字-美国键盘上为数不多的几个还没有C含义的符号之一-以及独特的基于括号的“Message Send”语法,需要一段时间才能习惯:

//Objective-CNSString*fileStr=[[NSString alloc]initWithData:fileContents编码:NSUTF8StringEncoding];//伪Swiftlet fileStr:NSString=NSString.alloc().initWithData(fileContents,编码:NSUTF8StringEncoding)//实际Swiftlet fileStr=NSString(data:fileContents,

人们第一次看到Objective-C语法时几乎普遍认为它很难看,几年后几乎普遍认为它完全正常,而且很大程度上(尽管不是几乎普遍)发现习惯用法Swift更容易阅读,即使他们已经习惯了Objective-C。

从这一点开始,您应该阅读Rust语法,而不需要循序渐进的解释。我会试着为我的Swift和其他非Rust的读者解释一下发生了什么-我自己对于Rust来说还是一个相对较新的人-但它会相当快的。这不是对Rust或Rust宏的介绍!

鉴于Objective-C运行时公开了一个公共API,我们应该可以很好地从Rust调用它,而且确实可以:

使用std::ffi::cstr;#[repr(C)]struct ObjCObject{ISA:ISIZE}#[repr(透明)]#[派生(克隆,复制)]结构选择器(*const U8);#[link(name=";objc";)]extern";C";{fn sel_registerName(name:*const U8)->;Select。Fn objc_getclass(name:*const U8)->;option<;&;';static ObjCObject>;;fn objc_msgSend();//参见下面}fn main(){#[link(name=";Foundation";Kind=";framework";)]extern{}//获取函数指针以便稍后转换。让msg_send=objc_msg作为不安全的外部发送";C";fn();不安全{让url_class=objc_getclass(";NSURL\0";。As_ptr())。UNWRAP();让description_sel=sel_registerName(";description\0";。As_ptr());let description_method=std::mem::transmute::<;_,unsafe extern";C";fn(_,_)->;_>;(Msg_Send);let description_obj:*const ObjCObject=description_method(url_class,description_sel);让utf8_sel=sel_registerName(";UTF8String\0";。As_ptr());let UTF8_Method=std::mem::transmute::<;_,unsafe extern";C";fn(_,_)->;_>;(Msg_Send);let UTF8_ptr=UTF8_Method(description_obj,UTF8_sel);println!(";{}";,CSTR::FROM_PTR(UTF8_PTR)。To_string_lossy());}}。

现在,这段代码应该会让大多数Rust用户感到非常震惊。开始时一切正常,先声明了一些类型,然后从libobjc声明了C API的Rust版本。然后它有一个有趣的空外部块来链接基础框架,但是当然,这没问题。它有显式以null结尾的字符串,因为这是基于C的API通常使用的。(默认情况下,Rust不保证空值终止,这使得分割Rust字符串变得更容易。)

但是还有一行异常的代码涉及std::mem::Transmute,相当于Rust的SWIFT的unsafeBitCast(_:)或C++的represtrate_cast(现在是bit_cast)或C的…。好吧,好吧,C没有直接的等价物,但是当你像这样谈论函数指针的时候,有一个普通的老式类型转换。变形术的文档甚至告诉你,如果可能的话,可以使用其他东西。

那么,我们在做什么呢?还记得我说过的Objective-C和objc_msgSend:方法是常规的C函数,而objc_msgSend“直接跳转到适当的、多态选择的方法实现”。这意味着调用objc_msgSend的正确方式是假装它具有您要调用的方法的正确类型。

在C中(就这一点而言,还有SWIFT),转换需要显式指定所有参数和返回值的类型。但是Rust在函数体中有非常强大的类型推断,这扩展到只指定一些泛型参数,而省略了其他参数。2在本例中,我需要指定我们要强制转换为具有两个参数和非空返回的C函数指针,但仅此而已。其余信息将根据函数指针的使用方式进行填充。

这样,您应该理解上面的(受诅咒的)代码。您先请。在您自己的机器上试用(如果您的机器是Mac)。

Rust有一个相当健壮的宏系统,所以为“消息发送”创建一个宏并不是不可能的,甚至不会太难。这就是Objc板条箱要走的路线:

让url_class=class!(NSURL);let description_obj:*mut object=msg_send![URL_CLASS,Description];let UTF8_ptr=msg_send![description_obj,UTF8String];println!(";{}";,cstr::from_ptr(UTF8_Ptr)。To_string_lossy());

但是,虽然这显然是严肃的防锈工作的正确选择,但我想要更雄心勃勃的东西。更具整合性。更多…。太荒谬了。

我想要的是Objective-C的消息传递语法或类似的语法在Rust代码中的任何位置都有效。它不必完全是Objective-C,但我很快意识到Objective-C语法的优势:它是有分隔符的,也就是说,它是一个自包含的表达式,可以放入更大的对象中,而不会更改该更大对象的解析方式。(这可能就是为什么它也用C括起来的原因。)。在括号内,基本上有两种形式:

如果我只是进行匹配,我就可以使用Rust原来的模式匹配宏。但是要获取整个代码块,并替换该块…中看起来像消息锁的所有内容。嗯,使用模式匹配和相当多的递归可能是可能的,但是使用过程化宏会容易得多,过程化宏是用Rust编写并用作编译器插件的Rust宏接口。(SWIFT人员,基本上是SWIFT语法允许您执行的操作,但在编译期间按需调用。)。

我看到的过程性宏示例要么是模式匹配宏的稍微更精细的版本,要么是完全不使用Rust语法的完整嵌入式DSL。但是像这样创建查找和替换宏是完全可能的;它只是意味着一些递归。

#[PROC_MACRO]pub FN MY_MACRO(TOKENS:TokenStream)->;TokenStream{//第一次递归...。让new_stream=tokens。INTO_ITER()。Map(|tree|{if let TokenTree::group(Group)=&;tree{let new_content=my_宏(group.。Stream());让mut result_group=Group::New(GROUP。分隔符(),NEW_CONTENTS);RESULT_GROUP。Set_span(组。Span());TokenTree::GROUP(RESULT_GROUP)}Else{tree}});//...然后进行实际工作。NEW_STREAM。Map(|tree|{//(可能比这个更有趣)tree})。Collect()}。

那是什么工作?那么,如果我们(1)处理的括号中的Group(2)与消息发送的语法匹配,那么我们应该生成类似于上面的手动代码的代码。我使用QUOTE CARATE做到了这一点,这是一个非常聪明的库,可以将RuST代码转换成…。铁锈代码。而是使用变量替换。3个。

让msg_expr=QUOTE!{让Receiver=#Receiver;let cmd=objc_rust::selector::get(#selector);让imp=objc_rust::objc_msg_lookup(Receiver,cmd);让function=unsafe{std::mem::transmute::<;_,extern";C";Fn(*const objc_rust::ObjCObject,objc_rust::选择器#(,#下划线)*)->;_>;(Imp)};function(Receiver,cmd#(,#参数)*)};

这与手动代码基本相同,但有一些有趣的注意事项:

不安全仅限于召唤变形。这意味着如果您在计算参数时做了不安全的事情,您仍然必须在您自己的代码中声明不安全。当然,您可以很容易地争辩说,调用Objective-C完全是不安全的,特别是因为编译器只相信您使用的类型。但是,这只是一个玩具,这一点仍然可能与其他人相关。

与以前不同的是,我们使用的是助手函数objc_msg_lookup,而不是直接访问objc_msgSend。这是因为objc_msgSend不是Objective-C中唯一的消息分派方法;还有objc_msgSend_Stret和另外两个仅在某些平台上极少数情况下出现的方法。为什么?因为在某些平台上,C函数的调用约定取决于返回类型,而objc_msgSend本身在执行查找之前不知道我们调用的是什么方法。我没有处理这个问题,而是通过与函数调用分开进行查找来回避这个问题。这会慢一点,不过还是要说一遍,玩具。4.

执行函数调用意味着传递未知数量的参数,这些参数被引用!易于操控。但是Transmut还需要知道参数的数量,这是一个更大的挑战。因此,下划线只是N个假下划线标记的迭代器,这些标记填充在接收器和选择器类型之后。在那件事上我感到相当聪明。:-)。

这就是我在这里要展示的所有代码。如果需要,您可以查看完整的proc_宏实现。

SYN、QUOTE和proc-mac2是编写过程性宏的首选程序库。它们使得定义自定义解析器、解析现有语法、创建新语法、处理旧版本的Rust等等都变得很容易!

内置的proc_宏和proc-acro2机箱处理基本的“词法分析”,包括匹配的圆括号、大括号和方括号,但其他内容不多。SYN可以获取来自PROC_MACRO的TokenStream,并从中解析实际的Rust语法树;QUOTE获取令牌和语法树,并将它们放回TokenStream形式。然而,有几件事让我在尝试使用这些工具时绊倒了:

属性样式宏必须以有效的Rust语法开头。RUST允许属性形式的宏以及显式宏调用,这将允许我编写。

并在没有第二层嵌套的情况下转换函数内的所有代码。但是,遗憾的是,关键在于Objective-C消息语法不是现有的Rust语法,因此只有普通的proc_宏形式有效。我的同事(同样是Cassie)指出,这允许基于Rust语法构建的各种工具假定正常宏外的任何东西实际上都有有效的语法;只有在宏内才会出现奇怪的东西。

对此没有明确的错误消息;您只会得到语法无效的错误。这大概是因为编译器甚至没有在解析正常代码之前调用您的宏。

如果要调试过程宏,请使用eprintln!并且输出将包含在编译器的stderr中。这对我来说并不完全是“理解”,因为我碰巧在David Tolnay的“过程宏研讨会”repo自述文件中发现了它,尽管我实际上并没有看过那个研讨会中的练习。这里的另一个重要提示是,如果您想转储解析的语法树,则为syn启用额外的特征特性。

要使用syn解析自定义语法,您必须创建一个实现Parse特征的新类型。这是让syn为您提供ParseStream的唯一方法,它包含解析Rust语法的所有有用方法。此外,据我所知,在执行此操作时必须解析整个令牌流;如果希望在流中间获取一些令牌,则必须保存开头和结尾的令牌,以便稍后转储。(通过一次解析整个带括号的组,我避开了这一限制。)。

使用syn解析表达式需要";完整";功能。我很高兴我记得功能标志是存在的,因为它们不在SWIFT生态系统中作为依赖项,但是在我将其添加到我的Cargo.toml配置文件表达式之前,它们立即无法解析。

如果宏引入了更多依赖项,则这些依赖项必须位于单独的模块中。那应该是你的主要售货库。与SWIFT包不同,Rust板条箱每个板条箱只能有一个库,而且程序宏已经不同于普通库,因为它们被构建为作为编译的一部分运行。因此,最简单的做法是将宏放入嵌套在主箱子中的第二个箱子中:5。

Inline-objc├──cargo.toml├──src│,├──lib.rs│,└──main.rs├──测试└──宏,├──cargo.toml,└──src,└──lib.rs。

并且inline_objc应该重新导出宏,这样客户端甚至不需要考虑所有这些:

(并且main.rs文件不是必需的;它只是用于快速测试。)。

这就是我目前掌握的全部内容,尽管我对Rust还比较陌生,所以我偶然发现了一些更有经验的Rust程序员/货物用户应该已经知道的事情。

铁锈博客有一个高级别总结,名为《铁锈2018年的程序性宏观》(ProceduleMacros In Rust 2018)。

如前所述,David Tolnay为学习过程性宏做了一次研讨会报告。我没有亲身经历过,但如果你想对实践项目进行适当的介绍,这可能是一个很好的起点。

对于更严肃的Rust/Objective-C互操作工作,有真正的Objc板条箱,以及Sheldon附带的博客文章,它比本文更循序渐进。

当我在推特上谈论这个玩具项目时,Ryan McGrath用他自己的有趣的实验Cacao作为回应,它建立在Objc的基础上,为AppKit和UIKit提供了铁锈包装。我当时并不知道,但是Servo项目有他们自己的项目来包装AppKit,core-Foundation-rs。

最后有趣的是,Mara Bos制作了一个箱子,允许在Rust代码中内联Python代码,尽管它(明智地)没有混合Python和Rust语法。她详细介绍了Python的实现!在她的博客上。

在上面,我提到Objective-C是一种“其功能(几乎)在运行时库中都可用”的语言。斯威夫特(Swift)也是这样吗,或者就这一点而言,拉斯特(Rust)也是如此?怎样才能实现SWIFT/Rust互操作?

唉,事实并非如此。Swift和Rust都超越了C支持的类型和操作集,远远超出了Objective-C的“强制转换objc_msgSend to the type of the method of you实际调用”的范围,并且(不必)在运行时提供有关该类型的信息。这其中有很多部分:

SWIFT和Rust都支持具有有效负载的枚举类型,它们智能地使用枚举中类型的属性来最小化默认使用的内存量。当然,他们的做法不同。

SWIFT和Rust都有不同于C语言(以及彼此)的函数调用约定,允许某些类型的调用比其他方式更高效。

SWIFT和Rust都有关于复制数值时会发生什么的规则,而不仅仅是“复制表示中的位”。在许多情况下,此信息仅在编译器中可用。

SWIFT和Rust都没有通用的“在此类型上按名称调用函数”操作;您必须通过协议/特征。

但并不是所有的希望都破灭了!即使您不能神奇地从Rust中调用Swift,或者从Swift中调用Rust,类似绑定的方法仍然是可能的,我的同事Nikolai Vazquez在这方面做了一些探索性的工作。此外,SWIFT确实为以某种动态方式使用的类型保留了相当数量的“反射”信息,这些信息至少可用于处理一部分类型。(据推测,拉斯特对Dyn特征也做了类似的事情。)。同时,这两种语言都支持使用C接口,因此这将在一段时间内成为桥梁。

如果你对这些东西感兴趣,你还应该看看亚历克西斯·贝因斯纳(Gankra)从铁锈的角度对斯威夫特静态和动态本质的评论。

以前是“OSX”,以前是“MacOSX”。这发音是“Ten”,而不是“ex”!-↩︎。

为什么SWIFT下面没有全功能类型。

.