作者:張健
公眾號:平安科技銀河安全實驗室
Cython是一種方便開發者為Python寫C extensions的語言,降低了開發者寫C拓展的難度;Cython module可以是.py或者.pyx文件;
編譯Cython module的主要過程:
1. Cython compiler將.py/.pyx文件編譯為C/C++文件;
2. C compiler再將C/C++編譯為.so(windows 為.pyd);
通過Cython將.py轉化為動態共享庫來發布,不僅能夠獲得性能的提升,從安全的角度來看,還能有助于保護源碼.
1、Cython的基本用法
編寫測試代碼hello.py:
def say_hello_to(name):
print("Hello %s!" % name)
相同目錄下新建setup.py:
from distutils.core import setup
from Cython.Build import cythonize
setup(name='Hello world',
ext_modules=cythonize("hello.py"))
編譯hello.py

import使用生成的hello module

2、分析Cython編譯生成的hello.c
2.1 initialization function(module入口函數)
hello.c其實是C/C++ extension,首先,發現hello.c中存在initialization function:



A.若python版本>=3,入口函數名字為:PyInit_##modulename,
否則,為init##modulename;
B.若python版本>=3,并且開啟了PEP489_MULTI_PHASE_INIT時,入口函數返回了PyModuleDef的對象指針;并且定義了pymod_exec函數;
否則,入口函數的函數體就是pymod_exec函數,返回初始化完成的Module;
Python官方文檔Building C and C++ Extensions介紹了入口函數的命名規則;
C.分析發現入口函數在import過程中被PyImport_LoadDynamicModuleWithSpec()調用:

2.2 PEP 489:Multi-phase extension module initialization
PEP489重新設計了extension module和import機制的交互過程,提出了多階段初始化,并且向后兼容single-phase initialization;
在入口函數之后,extension module的創建被分成了兩個階段:
module creation phase,module execution phase;
A. 入口函數
對PyModuleDef對象進行初始化,并返回對象指針;

__pyx_moduledef的類型就是PyModuleDef,結構如下:
注意到第二個成員即是module name;


extension module兩個階段處理過程就定義在PyModuleDef的m_slots成員中;
B.module creation phase
由 Py_mod_create slot管理,value所指向的函數有如下簽名:
PyObject (PyModuleCreateFunction)(PyObject spec, PyModuleDef def)
創建并返回一個Module Object,第一個參數是ModuleSpec類型指針,在PEP451中定義;
上述例子相對應的函數如下:

C.module execution phase
由Py_mod_exec slot指定,value所指向的函數有如下簽名:
int (PyModuleExecFunction)(PyObject module)
完成Module的初始化工作;
上述例子相對應的函數為:__pyx_pymod_exec_hello;

2.1中也提到single-phase initialization情況下,__pyx_pymod_exec_hello就是入口函數;
如果是multi-phase,直接使用module creation phase創建的module進行后續的初始化;
若python版本小于3,調用Py_InitModule4()生成module,第一個參數是module name;
否則,調用PyModule_Create(),參數為前面PyModuleDef對象;

后續module初始化完成后,會將此module加入sys.modules中,并且以module name為key;

3、多個Cython Module轉化成單個.so
Cython將單個.py轉化為單個.so比較方便,但是對package的支持卻不夠;package中存在多個.py和子目錄,其子目錄里面又包含多個.py和子目錄;這種情況下將每個.py轉化為一個.so,不便于后續對.so的加固保護。那么如何將package編譯成一個.so?
3.1 PEP 302:New Import Hooks
參考Collapse multiple submodules to one Cython extension,第二個回答提到import一個模塊時,Python會通過遍歷sys.meta_path中的finder來確定一個module相對應的loader,import機制在PEP 302中引入了sys.meta_path,finder,loader;
往sys.meta_path中注入module定制化的finder,finder需要實現find_module(),返回module定制的loader對象;而loader需要實現load_module(),完成對模塊的導入,并且返回module對象;
3.2 imp.load_dynamic()
參考的第二個答案基于importlib給出了python3的實現,在python2中,importlib并沒有“MetaPathFinder”等類,不過python2中提供了imp.load_dynamic(name, pathname[, file])從動態庫里來初始化module,且返回module對象;imp.load_dynamic官方文檔中有如下說明:
The pathname argument must point to the shared library. The name argument is used to construct the name of the initialization function: an external C function called initname() in the shared library is called. The optional file argument is ignored.
也就是說該函數有三個參數:module的名稱name,動態庫的路徑pathname,以及1個可忽略的參數file;
CPython implementation detail: The import internals identify extension modules by filename, so doing foo = load_dynamic("foo", "mod.so") and bar = load_dynamic("bar", "mod.so") will result in both foo and bar referring to the same module, regardless of whether or not mod.so exports an initbar function.
分析imp.load_dynamic()的源碼發現:
Python2中維持了一個 dictionary:extensions ,以pathname為key,用來記錄已經加載的動態庫,作用就是防止多次加載同一個動態庫執行其中的入口函數; imp.load_dynamic 會調用 _PyImport_LoadDynamicModule(), _PyImport_LoadDynamicModule() 會調用_PyImport_FindExtension() 來查詢 extensions 中緩存的 pathname 相對應的 module;如存在,就不會調用入口函數。
此外還發現,file參數存在的情況下,imp.load_dynamic會取得file的文件描述符’fp’,進而確定該文件的設備和inode編號,若已加載過該動態庫文件,最終會通過dlsym(filehandle,funcname)查找入口函數,并返回入口函數指針;



所以,不同的module,調用imp.load_dynamic()時,設置好file參數,pathname保持不同,設置為module的完整名即可,就可實現從同一個so中加載不同的module;
3.3 module名稱的調整
2.1節的hello.py,其python2入口函數名為:inithello;package中同一目錄下,python文件名不同,所以能夠保證入口函數名稱不同;但如果其子目錄下面存在同名的python文件,就會導致入口函數名沖突。
如下面的例子:

foo/foo1.py和foo/bar/foo1.py就會沖突,都是initfoo;
為了解決此問題,我們可以利用module包含package的完整名來重命名入口函數,將完整名中的點“.”換成下劃線“_”;這樣入口函數分別變為:initfoo_foo1和initfoo_bar_foo1,對于import機制來說module名就變為:foo_foo1和foo_bar_foo1,module完整名就分別變成:foo.foo_foo1和foo.bar.foo_bar_foo1。
3.4 python2下的實現
基于Collapse multiple submodules to one Cython extension的第二個回答,我們對Cython和bootstrap.py做了如下修改;
3.4.1 Compiler & ModuleNode.py的修改
對Cython Compiler的分析,發現2.1中hello.c代碼生成部分由ModuleNode.py負責;
A. 入口函數名調整為下劃線形式

ModuleNode.py對應的部分調整:

B. __pyx_pymod_exec_hello(),single-phase initialization情況下的入口函數函數體

ModuleNode.py對應的部分調整:

3.4.2 bootstrap.py & setup.py
根據前面3.2 bootstrap.py下:
import sys, imp
class CythonPackageFileLoader():
def __init__(self):
pass
def load_module(self, fullname):
print('load_module: '+fullname)
sub_name = fullname.replace('.', '_')
package = fullname.rsplit('.', 1)[0]
new_name = package + '.' + sub_name
if new_name in sys.modules:
print('found in sys.modules')
return sys.modules[new_name]
module = imp.load_dynamic(new_name, new_name, file(__file__))
module.__file__ = __file__
module.__loader__ = self
module.__package__ = package
print(module)
\#print(sys.modules.keys())
\#sys.modules[new_name] = module
return module
class CythonPackageMetaPathFinder():
def __init__(self, name_filters):
self.name_filters = name_filters
self.loader = CythonPackageFileLoader()
def find_module(self, fullname, path=None):
print('find_module: '+fullname)
for name_filter in self.name_filters:
if fullname == name_filter:
return self.loader
return None
def bootstrap_cython_submodules():
sys.meta_path.append(CythonPackageMetaPathFinder(
[
'foo.foo1',
'foo.foo2',
'foo.bar.bar1',
'foo.bar.foo1'
]
))
setup.py
\#!/usr/bin/env python2
from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize
extension = Extension("foo.foo_bootstrap",
[
"foo/bootstrap.py",
"foo/foo1.py",
"foo/foo2.py",
"foo/bar/bar1.py",
"foo/bar/foo1.py",
],
extra_compile_args=['-DCYTHON_PEP489_MULTI_PHASE_INIT=0', '-g']
)
setup(
name = 'cython_test',
ext_modules = cythonize(extension)
)
3.5 python3下的實現
3.5.1 PEP 451:A ModuleSpec Type for the Import System
PEP 451提出了向importlib.machinery添加一個名為“ModuleSpec”的新類。它將提供用于加載一個module的所有導入相關的信息,且無需首先加載module即可使用。finder將直接提供module對應的ModuleSpec對象,而不是loader(通過ModuleSpec間接提供)。import機制將進行調整以利用ModuleSpec的優勢,使用它們來加載模塊。
finder和loader基于此PEP需要進行相應的調整:
1、定制module對應的finder,并且實現其find_spec(),返回對應的ModuleSpec對象,取代其find_module();
2、定制loader,盡可能實現其exec_module(),取代load_module();
3、當然,PEP 451向后兼容PEP 302;
importlib里面新增了一些api和類,方便我們實現finder及loader;
A.通過importlib.machinery.ExtensionFileLoader(fullname, path)來實現loader;
B.通過importlib.util.spec_from_loader(name, loader, *, origin=None, is_package=None)來生成封裝了loader的spec;
C.繼承importlib.abc.MetaPathFinder實現finder,其find_spec()執行上述兩步;
module name應該為下劃線格式的新名字,且為包含package的完整名;
基于Collapse multiple submodules to one Cython extension的第二個回答,利用ModuleSpec,我們對Cython和bootstrap.py做了如下修改;
3.5.2 Compiler/ModuleNode.py的修改
A、入口函數修改為下劃線格式

ModuleNode.py對應的部分調整:

B、PyModuleDef類中的m_name
PyModule_Create()源碼中指出,m_name是不帶package的module name;


ModuleNode.py對應的部分調整:

C、__pyx_pymod_exec_hello():校驗及加入sys.modules;


3.5.3 bootstrap.py & setup.py
bootstrap.py:
import sys
import importlib.abc
import importlib.util
class CythonPackageMetaPathFinder(importlib.abc.MetaPathFinder):
def __init__(self, name_filters):
super(CythonPackageMetaPathFinder, self).__init__()
self.name_filters = name_filters
def find_spec(self, fullname, path, target):
print('find_spec: '+fullname)
for name_filter in self.name_filters:
if fullname==name_filter:
\# foo.foo1 -> foo.foo_foo1
sub_name = fullname.replace('.', '_')
new_name = fullname[:fullname.rfind('.')+1] + sub_name
\#print('new_name: '+new_name)
if new_name in sys.modules:
return sys.modules[new_name].__spec__
loader = importlib.machinery.ExtensionFileLoader(new_name,__file__)
spec = importlib.util.spec_from_loader(new_name, loader)
print(spec)
return spec
return None
\# injecting custom finder/loaders into sys.meta_path:
def bootstrap_cython_submodules():
print('in foo bootstrap '+__file__)
name_filters = [
'foo.foo1',
'foo.foo2',
'foo.bar.foo1',
'foo.bar.bar1'
]
sys.meta_path.append(CythonPackageMetaPathFinder(name_filters))
setup.py:同python2
參考文獻:
-
https://github.com/python/cpython/blob/master/Python/importdl.c#L134
-
https://stackoverflow.com/questions/30157363/collapse-multiple-submodules-to-one-cython-extension
-
https://github.com/python/cpython/blob/2.7/Python/import.c#L3150
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1139/