较快的Python与Go Shared对象(简单的方式)

2021-04-15 22:13:56

当探索如何在CTFD内容编辑器中探讨潜在恶意HTML内容时,我觉得这尤其觉得这一点。 使用可用的HTML4解析器解析HTML不好(例如,HTML.Parser,LXML'默认选项) 使用HTML5LIB(几乎所有Python HTML5解析器依赖于HTML5LIB)慢慢地解析HTML5 我开始尝试许多传统技术,看看我是否可以加快HTML处理。 使用更高效的Python语言构造并尝试重写代码以不慢慢🧠 我知道另一种加快Python的方法是重写另一种语言的所有东西都以另一种语言重写慢速代码,并从Python调用该代码。 如果我' m试图编写更快的python它,因为我在第一个地方没有真正想要写下C.

你会是对的!你真的想写C代码吗?或者CTYPES代码?

但是,请谈谈我们如何尽可能少的方式解决这一点。

Google' s go编程语言有能力生成可由其他应用程序加载的共享库/对象。然后,这些应用程序可以访问编译的GO代码,而无需进行Go编译器或了解任何关于Go的任何内容。

如果您已经知道,Python有能力从共享对象导入代码!它'这个功能很多图书馆利用,使某些部分" python"代码更快。这里的常态的差异是我们'重新使用Go生成共享对象。

该策略具有一些福利和缺点,应该向前表示。大多数其他帖子讨论这个话题并不清楚缺点,我认为他们中的一些是为什么我们'尽管它是一个非常有能力的选择,但是没有看到更多的Python模块。

复杂任务(如HTML解析)的内存损坏的风险远远不如C

将为您生成很多代码,所以您'只有真正写作python并走

它难以通过golang和python之间的非原始类型。或者至少我哈登' t正确效果了。

Python包装已经有点努力,并加入混合使其变得更加困难。 Go' aren'它有用,但它们可能是未来的帮助。

显然,您应该在特定情况下使用此技术。我总结了一些'&#39的工作良好,但你应该评估和基准它是否有意义。

我要忽略谈论它,但你应该得到一个工作的Go环境设置。如果你可以运行一个Go Hello World榜样,你'很高兴去。不幸的是,这篇文章赢得了' t是一个良好的学习资源,但是有一个更好的地方。

这段代码显然只是打印"你好世界!"调用hello()函数时。然而,魔法有两行:

导入C启用转到C调用C代码,但也告诉编译器生成从C.相关的Go文档调用Ro代码的标题文件

导出线指示Go编译器将函数导出到生成的标题文件下面的注释下方,因此可以通过其他应用程序调用它。相关GO文件

接下来,我们将编译此代码。在与上述hello.go文件中的目录中运行以下命令。

-BuildMode = C-Shared构建列出的主包,加上它导入的所有包,进入C共享库。唯一可调用的符号将是使用cgo //导出注释导出的函数。需要恰好列出一个主包。

我们赢得了'甚至使用标题文件,但删除了' t删除它。我们需要在下一节中复制标记为extern的函数。在这种情况下,我们会关心extern int hello();靠近底部的线。

现在,您可以在技术上使用CTypes与生成的共享对象进行接口。

❯ipythonpython 3.8.5(默认,7月21日,10:48:26)类型'版权所有''信用'或#39;许可证'有关更多信息,但是一个增强的交互式Python。类型'?'对于help.in [1]:导入ctypesin [2]:hello = ctypes.cdll(" hello.so")[3]:hello.hello()您好世界!OUT [3]:0

包MainImport" C" //导出hellofunc hello(name * c.char)* c.Char {goname:= c.gostring(name)结果:="你好" + goname返回c.cstring(结果)} func main(){}

这开始使我们的原始示例复杂化,并开始揭示写入的困难。

如果我们想通过数据,我们需要使用通过提供的C类型。这些aren'通过go凭证,但有一些在线参考。在我们的示例中,我们的功能需要接受和返回* C.Char。

然后使用那些C类型作为GO变量,我们需要将其转换回与C.GoString的Go String

最后要将其退回为* C.Char,我们需要使用C.cstring()转换转换串。

但是c.cstring使用malloc()创建一个字符串,因此需要释放(我们没有' t)。去不会警告你这个错误。只有你可以防止森林火灾内存泄漏。这里的一篇零体'

//使用malloc.//将C字符串分配使用Malloc.//是呼叫者'责任安排它以//释放,例如通过调用C.free(务必包括STDLIB.H) //如果需要c.free)。

我们再次使用ctypes调用此示例,但它也会变得更加复杂。既然我们有参数和返回值,我们需要为Python提供类型。

导入ctypeshelo = ctypes.cdll(" hello.so")#指定argumentshello.hello.argtypes = [ctypes.cr_char_p]#指定return valuehello.hello的类型的列表。restype = ctypes.c_char_p #edode我们的字符串进入字节名称=" dude" .encode(" utf-8")#调用函数并存储返回的valueresponse = hello.hello(姓名)打印(响应)#b'您好Dude'

这是' t太糟糕但它非常讨厌使用ctypes api重写我们的类型声明。特别是作为我们加薪的职能量。其中'你的机会' ll获取错误类型的声明。我不知道那种情况下会发生什么。

CFFI库允许我们更轻松地与共享对象进行接口,让我们直接从Go' s生成的头文件中复制类型规范。它还具有函数,让我们更轻松地将返回的指针转换为适当的Python数据结构。

所以让' s跟随cffi' s"主要用途模式"建立一个辅助共享对象,将使用CFFI和#39;胶水代码包装我们现有的共享对象。

来自CFFI Import FFiffibuilder = FFI()#指定由Go Compiler for Source#指定由Go Compiler for Extra_ObjectSFFibuilder.set_source(module_name =&#34生成的共享对象(module_name =" hello",来源=""#34; #include" hello.h""""""" hello.so&#34 ;],)#在头文件底部(例如hello.h)ffibuilder.cdef(csource =""" extern char * hello(char * p0); """)如果_nname__ ==" __ main __&#34 ;: ffibuilder.compile(verbose = true)

我们希望通过必要的细节提供CFFI,以便能够构建包装包共享模块,以便我们需要提供:

我们希望能够调用的共享对象功能列表。这已经由底部Hello.h文件附近的Go Compiler为您生成。你可以复制&粘贴它来build_ffi.py。也许你可以自动提取它,但是为后来的项目' s。

❯python build_ffi.pygeneration ./hello.c(最新最新)运行build_extbuilding'你好' ExtensionClang -wno-uneused-result -wsign-compare -wunReachable-code -fno-common -dynamic -dndebug -g -fwrapv -o3 -wall-isysroot /library/developer/commandlinetools/sdks/macosx10.15.sdk -i /库/开发人员/ commandlinetools / sdks / macosx10.15.sdk / usr / compress -i / library / developer / commandlineTools / sdks / macosx10.15.sdk / system / library / frameworks / tk.framework /版本/ 8.5 /标题 - i/usr/local/cellar/[email protected]/3.8.5/frameworks/python.framework/versions/3.8/include/python3.8/clude/python3.8 -c hello.c -o ./hello.oclang -bundle -undined dynamic_lookup-isysroot /library/developer/commandlinetools/sdks/macosx10.15.sdk ./hello.o hello.so -o ./hello.cpython-38-darwin.so

我们关心新的hello.cpython-38-darwin.so文件。 根据您的操作系统,姓名再次可能不同。 来自你好导入lib,ffiname = b" guy" r = lib.hello(name)打印(r)#< cdata' char *' 0x7FF309706230>打印(ffi.string(r))#b'你好guy' 基本上我们的导出功能可在lib模块上使用,FFI模块提供转换&amp的功能; 在C&amp之间创建; Python类型。 在CFFI文档中指定了与生成的共享对象交互的完整过程。 现在,您可以采取生成的共享对象并编写简单的Python包装代码。 用户赢得了'它必须知道实际代码的黑暗秘密,即使在Python中也是如此。 围绕本主题的大多数现有讨论只需在此停止。 它完美的,他们的Hello World Golang模块在生产中幸福地运行,在水平中幸福地运行。 这不是我的情况,因为我遇到了Go FFI的一些问题:

包MainImport" C" //导出hellofunc hello(name * c.char)* c.Char {goname:= c.gostring(name)结果:="你好" + goname返回c.cstring(结果)} func main(){}

我提到由于C.cstring()的用法而没有后续免费(),此代码具有内存泄漏。你如何解决这个问题?

好的,但如果我这样做,我如何将数据返回到我的模块?我回来后有没有办法释放?

如果您更深入地挖掘,您将找到将在周围函数返回之前推迟执行函数的Defer语句。

package main // #include< stdlib.h>进口" c"进口"不安全" func main(){cs:= c.cstring("你好,来自stdio&# 34;)//等待函数在释放前返回d.free(不安全.pointer(CS))}

此策略的问题(如果实施)是Python将持有悬挂指针。指针指向已被释放的内存位置,以便在任何时候都可能更改。在这里是他们说的龙。

相反,我解决了什么是要导出C.Free()包装函数,我可以在我复制数据后从Python调用。所以例如:

package main // #include< stdlib.h>进口" c"进口"不安全" //导出hellofunc hello(name * c.char)* c.car {goname:= c.gostring(姓名)结果:="你好" + goname return c.cstring(结果)} //导出freecstringfunc freeCstring(s * c.char){c.free(Unseafe.pointer(s))} func main(){}

来自你好导入lib,ffiname = b" guy"#存储一个指针到return valure r = lib.hello(name)#popy the字符串valuevalue = ffi.string(r)#free the pointerlib.freecstring( r)#值仍然可用(value)#您还可以验证:# - id()是不同的# - cffi使用正确的python c api函数复制#https://foss.heptapod.net/ pypy / cffi / - / blob / branch / default / cffi / api.py#l302-318#https://foss.heptapod.net/pypy/cffi//blob/branch/default/c/_cffi_backend.c# l6749-6859#https://docs.python.org/3/c-api/bytes.html#c.pybytes_fromstringandsize.

正如我之前提到的那样,它难以在Python之间传递非原始数据。您'重复的东西仅限于像短,int,long,char,float,double和其他东西的东西。

假设您创建了一个很好的Go struct(基本上是一个类),其中包含所有字段和方法。如果你和我这样的初学者,你可能会认为你可以在某种程度上致法地将结构传递给Python并像常规Python类或其他东西一样使用它。

我发现最简单的策略是在Python侧的Go Side和Python对象中保留Go对象并将原始引用相互传递给彼此。

package main / * #include< stdlib.h> * /导入" c"进口(" math / rand"" github.com/microcosm-cc/bluemonday" ;)var policies = map [uint32] * bluemonday.policy {} func getpolicyid()uint32 {penticeId:= rand.uint32()for {if policies [palictid] == nil {break} else {policyid = rand.uint32( }}}}}}} return prodationId} // export newpolicyfunc newpolicy()c.ulong {penticeId:= getpolicyid()策略:= bluemonday.newpolicy()策略[prodanicid] =策略返回c.ulong(policyId)} func main(){ }

这里的策略是我生成一个"随机"使用getpolicyid()和newpolicy()并将其传递给Python代码。同样,当我想在GO结构上调用方法时,我将标识符传递回DO代码以识别我要使用的结构的哪个实例,并使用反射来基于字符串I调用适当的方法提供。

这两种策略不一定是最好的,并且可能存在我遇到的问题' t尚未遇到过。例如,Python侧可以通过损坏_id属性或忘记释放内存来造成一些问题,但我尚未提出更好的解决方案。

最初我希望通过获取更快的语言(可能是RUDE)来在LXML' S的清洁模块上构建一些东西来进行处理,然后将LXML进行清洁。老实说,这永远不会在合理的时间内工作。

Bluemonday实现了基于白名单的HTML Sanitizer,内置于X / Net / HTML,Golang' S&#34之上; HTML5标准的销售器和解析器" Bluemonday提供了很多旋钮,可以为非常灵活的消毒方式调整,并正在使用包含用户贡献内容的大量项目。

PyBluemOnday使用此帖子中概述的相同技术实现Python绑定到Bluemonday。您可以将其视为这里讨论的一切的先进摘要。

我建立了一个小型基准测试脚本,可以将PyBluemonday的速度与标准Python HTML Soonitizer库进行比较,结果非常令人印象深刻:

您可以通过在GO之内保留所有处理和消毒逻辑来看,我们受益于显着的速度收益。基本上,我们只在&#39准备使用时将数据传递回Python。

这篇文章解决了很多初始困难,而我遇到了在Go写入Python模块时。它并不像在Python中写一切一样符合人体工程学,但我觉得它可能会在C编写R原C代码的难度下直接写入等数量的速度。

可能存在一些内存开销,因为Go运行时可能静态编译到共享对象中,但我认为它'是一个值得关注的价值。 此外,我没有写下这篇文章的方面,但PyBluemonday有用于所有主要目标的轮子(Linux,OSX,Windows)。 这是基于CibuildWheel和Setuptools-Golang的。 也许我将介绍如何在未来的帖子中这样做,因为它没有琐碎的设置。 我希望这篇文章能够对Python模块进行Go围绕的一些混淆,我希望这个过程随着时间的推移而变得更容易。