Crear enlaces de Python para bibliotecas C / C ++ usando SIP. Parte 1

A veces, mientras se trabaja en un proyecto en Python, existe el deseo de usar una biblioteca que no esté escrita en Python, sino, por ejemplo, en C o C ++. Las razones para esto pueden ser diferentes. Primero, Python es un lenguaje maravilloso, pero en algunas situaciones no es lo suficientemente rápido. Y si ve que el rendimiento está limitado por las características del lenguaje Python, entonces tiene sentido escribir parte del programa en otro lenguaje (en este artículo hablaremos de C y C ++), organice esta parte del programa como una biblioteca, haga enlaces de Python (enlaces de Python) encima y use el módulo así obtenido como una biblioteca Python normal. En segundo lugar, a menudo ocurre una situación cuando se sabe que hay una biblioteca que resuelve el problema requerido, pero, desafortunadamente, esta biblioteca no está escrita en Python, sino en el mismo C o C ++.En este caso, también podemos hacer un enlace de Python sobre la biblioteca y usarlo sin pensar en el hecho de que la biblioteca no se escribió originalmente en Python.

Existen varias herramientas para crear enlaces de Python, que van desde los de nivel inferior como Python / C API hasta los de nivel superior como SWIG y SIP .

No tenía el objetivo de comparar diferentes formas de crear enlaces de Python, pero me gustaría hablar sobre los conceptos básicos del uso de una herramienta, a saber, SIP . Inicialmente, SIP fue diseñado para crear un enlace alrededor de la biblioteca Qt - PyQt , y también se usa para desarrollar otras bibliotecas Python grandes, por ejemplo, wxPython .

En este artículo, gcc se usará como compilador para C y g ++ se usará como compilador de C ++. Todos los ejemplos fueron probados en Arch Linux y Python 3.8. Para no complicar los ejemplos, el tema de la compilación para diferentes sistemas operativos y el uso de diferentes compiladores (por ejemplo, Visual Studio) no está incluido en el alcance de este artículo.

Puede descargar todos los ejemplos de este artículo desde el repositorio en github .
El repositorio con fuentes SIP se encuentra en https://www.riverbankcomputing.com/hg/sip/ . Mercurial se utiliza como sistema de control de versiones para SIP.

Hacer un enlace sobre una biblioteca en C


Escribir una biblioteca en C


Este ejemplo se encuentra en la carpeta pyfoo_c_01 en la fuente, pero en este artículo asumiremos que estamos haciendo todo desde cero.

Comencemos con un ejemplo simple. Primero, crearemos una biblioteca C simple, que luego ejecutaremos desde un script Python. Deje que nuestra biblioteca sea la única función

int foo(char*);

que tomará una cadena y devolverá su longitud multiplicada por 2. El

archivo de encabezado foo.h puede verse, por ejemplo, así:

#ifndef FOO_LIB
#define FOO_LIB

int foo(char* str);

#endif

Y el archivo con la implementación de foo.cpp :

#include <string.h>

#include "foo.h"

int foo(char* str) {
	return strlen(str) * 2;
}

Para probar la funcionalidad de la biblioteca, escribiremos un programa main.c simple :

#include <stdio.h>

#include "foo.h"

int main(int argc, char* argv[]) {
	char* str = "0123456789";
	printf("%d\n", foo(str));
}

Para mayor precisión, cree un Makefile :

CC=gcc
CFLAGS=-c
DIR_OUT=bin

all: main

main: main.o libfoo.a
	$(CC) $(DIR_OUT)/main.o -L$(DIR_OUT) -lfoo -o $(DIR_OUT)/main

main.o: makedir main.c
	$(CC) $(CFLAGS) main.c -o $(DIR_OUT)/main.o

libfoo.a: makedir foo.c
	$(CC) $(CFLAGS) foo.c -o $(DIR_OUT)/foo.o
	ar rcs $(DIR_OUT)/libfoo.a $(DIR_OUT)/foo.o

makedir:
	mkdir -p $(DIR_OUT)

clean:
	rm -rf $(DIR_OUT)/*

Deje que todas las fuentes de la biblioteca foo se encuentren en una subcarpeta de foo en la carpeta fuente:

foo_c_01 /
└── foo
    ├── foo.c
    ├── foo.h
    ├── main.c
    └── Makefile


Entramos en la carpeta foo y compilamos las fuentes usando el comando

make

Durante la compilación, se mostrará el texto.

mkdir -p bin
gcc -c main.c -o bin/main.o
gcc -c foo.c -o bin/foo.o
ar rcs bin/libfoo.a bin/foo.o
gcc bin/main.o -Lbin -lfoo -o bin/main

El resultado de la compilación se colocará en la carpeta bin dentro de la carpeta foo :

foo_c_01 /
└── foo
    ├── bin
    │ ├── foo.o
    │ ├── libfoo.a
    │ ├── principal
    │ └── main.o
    ├── foo.c
    ├── foo.h
    ├── main.c
    └── Makefile


Compilamos una biblioteca para enlaces estáticos y un programa que la usa bajo el nombre main . Después de la compilación, puede verificar que se inicie el programa principal .

Hagamos un enlace de Python sobre la biblioteca foo.

Conceptos básicos de SIP


Primero necesitas instalar SIP. Esto se hace de manera estándar, como con todas las otras bibliotecas que usan pip:

pip install --user sip

Por supuesto, si está trabajando en un entorno virtual, no se debe especificar el parámetro --user, que indica que la biblioteca SIP debe instalarse en la carpeta del usuario, y no globalmente en el sistema.

¿Qué debemos hacer para que se pueda llamar a la biblioteca foo desde el código Python? Como mínimo, debe crear dos archivos: uno de ellos en formato TOML y llamarlo pyproject.toml , y el segundo es un archivo con la extensión .sip. Tratemos con cada uno de ellos secuencialmente.

Necesitamos acordar la estructura de la fuente. Dentro de la carpeta pyfoo_c se encuentra la carpeta foo , que contiene la fuente de la biblioteca. Después de la compilación , la carpeta bin se crea dentro de la carpeta foo, que contendrá todos los archivos compilados. Más adelante agregaremos la capacidad para que el usuario especifique las rutas al encabezado y los archivos de objetos de la biblioteca a través de la línea de comando.

Los archivos necesarios para SIP se ubicarán en la misma carpeta que la carpeta foo .

pyproject.toml


El archivo pyproject.toml no es una invención de los desarrolladores de SIP, sino el formato de descripción del proyecto Python descrito en PEP 517 "Un formato independiente del sistema de compilación para árboles de origen" y en PEP 518 "Especificación de los requisitos mínimos del sistema de compilación para proyectos de Python" . Este es un archivo TOML , que puede considerarse como una versión más avanzada del formato ini, en el que los parámetros se almacenan en forma de "clave = valor", y los parámetros pueden ubicarse no solo en secciones como [foo], que se denominan tablas en términos TOML, sino y en subsecciones de la forma [foo.bar.spam]. Los parámetros pueden contener no solo cadenas, sino también listas, números y valores booleanos.

Se supone que este archivo describe todo lo que se necesita para construir un paquete de Python, y no necesariamente usando SIP. Sin embargo, como veremos un poco más adelante, en algunos casos este archivo no será suficiente y, además, deberá crear un pequeño script de Python. Pero hablemos de todo en orden.

Puede encontrar una descripción completa de todos los parámetros posibles del archivo pyproject.toml relacionados con SIP en la página de documentación de SIP .

Para nuestro ejemplo, cree el archivo pyproject.toml al mismo nivel que la carpeta foo :

foo_c_01 /
├── foo
│ ├── bin
│ │ ├── foo.o
│ │ ├── libfoo.a
│ │ ├── principal
│ │ └── main.o
│ ├── foo.c
│ ├── foo.h
│ ├── main.c
│ └── Makefile
└── pyproject.toml


El contenido de pyproject.toml será el siguiente:

[build-system]
requires = ["sip >=5, <6"]
build-backend = "sipbuild.api"

[tool.sip.metadata]
name = "pyfoo"
version = "0.1"
license = "MIT"

[tool.sip.bindings.pyfoo]
headers = ["foo.h"]
libraries = ["foo"]
include-dirs = ["foo"]
library-dirs = ["foo/bin"]

La sección [build-system] ("tabla" en términos TOML) es estándar y se describe en PEP 518 . Contiene dos parámetros:


Otros parámetros se describen en las secciones [tool.sip. *] .

La sección [tool.sip.metadata] contiene información general sobre el paquete: el nombre del paquete que se construirá (nuestro paquete se llamará pyfoo , pero no confunda este nombre con el nombre del módulo, que luego importaremos a Python), el número de versión del paquete (en nuestro caso número de versión "0.1") y licencia (por ejemplo, " MIT ").

Lo más importante desde el punto de vista del ensamblaje se describe en [tool.sip.bindings. pyfoo ].

Anote el nombre del paquete en el encabezado de la sección. Hemos agregado dos parámetros a esta sección:

  • encabezados : una lista de archivos de encabezado necesarios para usar la biblioteca foo.
  • bibliotecas : una lista de archivos de objetos compilados para la vinculación estática.
  • incluir-dirs es la ruta en la que debe buscar los archivos de cabecera adicionales además de los que están unidos al compilador C. En este caso, dónde buscar la foo.h archivo .
  • library-dirs es la ruta donde buscar archivos de objetos adicionales además de los que están adjuntos al compilador de C. En este caso, esta es la carpeta en la que se crea el archivo de biblioteca compilado foo .

Entonces, creamos el primer archivo necesario para SIP. Ahora pasamos a crear el siguiente archivo que describirá el contenido del futuro módulo de Python.

pyfoo.sip


Cree el archivo pyfoo.sip en la misma carpeta que el archivo pyproject.toml :

foo_c_01 /
├── foo
│ ├── bin
│ │ ├── foo.o
│ │ ├── libfoo.a
│ │ ├── principal
│ │ └── main.o
│ ├── foo.c
│ ├── foo.h
│ ├── main.c
│ └── Makefile
├── pyfoo.sip
└── pyproject.toml


Un archivo con la extensión .sip describe la interfaz de la biblioteca fuente, que se convertirá en un módulo en Python. Este archivo tiene su propio formato, que ahora consideraremos, y se asemeja al archivo de encabezado C / C ++ con marcado adicional, que debería ayudar a SIP a crear un módulo de Python.

En nuestro ejemplo, este archivo debería llamarse pyfoo.sip , porque antes de eso, en el archivo pyproject.toml, creamos [tool.sip.bindings. pyfoo] En el caso general, puede haber varias particiones de este tipo y, en consecuencia, debe haber varios archivos * .sip. Pero si tenemos varios archivos sip, este es un caso especial desde el punto de vista de SIP, y no lo consideraremos en este artículo. Tenga en cuenta que, en el caso general, el nombre del archivo .sip (y, en consecuencia, el nombre de la sección) puede no coincidir con el nombre del paquete que se especifica en el parámetro de nombre en la sección [tool.sip.metadata] .

Considere el archivo pyfoo.sip de nuestro ejemplo:

%Module(name=foo, language="C")

int foo(char*);

Las líneas que comienzan con el carácter "%" se denominan directivas. Deben decirle a SIP cómo ensamblar y diseñar correctamente el módulo Python. En esta página de documentación se describe una lista completa de directivas . Algunas directivas tienen parámetros adicionales. Los parámetros pueden no ser necesarios.

En este ejemplo, usamos dos directivas; conoceremos algunas otras directivas en los siguientes ejemplos.

El archivo pyfoo.sip comienza con la directiva % Module (name = foo, language = "C") . Tenga en cuenta que especificamos el valor del primer parámetro ( nombre ) sin comillas, y el valor del segundo parámetro ( idioma) con comillas, como cadenas en C / C ++. Este es un requisito de esta directiva como se describe en la documentación de la directiva % Module .

En la directiva % Module , solo se requiere el parámetro de nombre , que establece el nombre del módulo Python desde el que importaremos la función de biblioteca. En este caso, el módulo se llama foo , contendrá la función foo , por lo que después del ensamblaje y la instalación lo importaremos utilizando el código:

from foo import foo

Podríamos hacer que este módulo esté anidado en otro módulo reemplazando esta línea, por ejemplo, con esto:

%Module(name=foo.bar, language="C")
...

Luego, importa la función foo debería ser la siguiente:

from foo.bar import foo

El parámetro de idioma de la directiva % Module indica el idioma en el que se escribe la biblioteca de origen. El valor de este parámetro puede ser "C" o "C ++". Si no se especifica este parámetro, SIP asumirá que la biblioteca está escrita en C ++.

Ahora mire la última línea del archivo pyfoo.sip :

int foo(char*);

Esta es una descripción de la interfaz de la función de la biblioteca que queremos incluir en el módulo Python. Según esta declaración, sip creará una función de Python. Creo que todo debería estar claro aquí.

Recopilamos y verificamos


Ahora todo está listo para compilar un paquete de Python con un enlace para una biblioteca C. En primer lugar, debe compilar la propia biblioteca. Vaya a la carpeta pyfoo_c_01 / foo / e inicie la compilación utilizando el comando make :

$ make

mkdir -p bin
gcc -c main.c -o bin/main.o
gcc -c foo.c -o bin/foo.o
ar rcs bin/libfoo.a bin/foo.o
gcc bin/main.o -Lbin -lfoo -o bin/main

Si todo salió bien, la carpeta bin se creará dentro de la carpeta foo , en la que, entre otros archivos, se compilará la biblioteca libfoo.a . Permíteme recordarte que aquí, para no distraerte del tema principal, solo estamos hablando de construir bajo Linux usando gcc. Regrese a la carpeta pyfoo_c_01 . Ahora es el momento de conocer los equipos SIP. Después de instalar SIP, los siguientes comandos de línea de comandos estarán disponibles ( página de documentación ):



  • sip-build . Crea un archivo de objeto de extensión de Python.
  • instalación por sorbo . Crea un archivo de objeto de extensión de Python y lo instala.
  • sip-sdist. .tar.gz, pip.
  • sip-wheel. wheel ( .whl).
  • sip-module. , , SIP. , , . , standalone project, , , , .
  • sip-distinfo. .dist-info, wheel.

Estos comandos deben ejecutarse desde la carpeta donde se encuentra el archivo pyproject.toml .

Para comenzar, para comprender mejor el funcionamiento de SIP, ejecute el comando sip-build , con la opción --verbose para obtener resultados más detallados en la consola, y vea qué sucede durante el proceso de compilación.

$ sip-build --verbose

Estos enlaces se construirán: pyfoo.
Generando los enlaces pyfoo ...
Compilando el módulo 'foo' ...
construyendo la extensión 'foo'
creando build
creando build / temp.linux-x86_64-3.8
gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -march = x86-64 -mtune = generic -O3 -pipe -fno-plt -fno-semantic-interposition -march = x86-64 -mtune = genérico -O3 -pipe -fno-plt -march = x86-64 -mtune = genérico -O3 -pipe -fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected = public -I. -I ../../ foo -I / usr / include / python3.8 -c sipfoocmodule.c -o build / temp.linux-x86_64-3.8 / sipfoocmodule.o
sipfoocmodule.c: en la función
func_foo : sipfoocmodule .c: 29: 22: advertencia: declaración de función implícita "foo" [-Wimplicit-function-declaración]
29 | sipRes = foo (a0);
El | ^ ~~
gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -march = x86-64 -mtune = generic -O3 -pipe -fno-plt -fno-semantic-interposition -march = x86-64 -mtune = genérico -O3 -pipe -fno-plt -march = x86-64 -mtune = genérico -O3 -pipe -fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected = public -I. -I ../../ foo -I / usr / include / python3.8 -c array.c -o build / temp.linux-x86_64-3.8 / array.o
gcc -pthread -Wno-unused-result -Wsign -comparar -DNDEBUG -g -fwrapv -O3 -Wall -march = x86-64 -mtune = genérico -O3 -pipe -fno-plt -fno-semantic-interposition -march = x86-64 -mtune = generic -O3 -pipe -fno-plt -march = x86-64 -mtune = genérico -O3 -pipe -fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected = public -I. -I ../../ foo -I / usr / include / python3.8 -c bool.cpp -o build / temp.linux-x86_64-3.8 / bool.o
gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -march = x86-64 -mtune = generic -O3 -pipe -fno-plt -fno-semantic-interposition -march = x86-64 -mtune = genérico -O3 -pipe -fno-plt -march = x86-64 -mtune = genérico -O3 -pipe -fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected = public -I. -I ../../ foo -I / usr / include / python3.8 -c objmap.c -o build / temp.linux-x86_64-3.8 / objmap.o
gcc -pthread -Wno-unused-result -Wsign -comparar -DNDEBUG -g -fwrapv -O3 -Wall -march = x86-64 -mtune = genérico -O3 -pipe -fno-plt -fno-semantic-interposition -march = x86-64 -mtune = generic -O3 -pipe -fno-plt -march = x86-64 -mtune = genérico -O3 -pipe -fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected = public -I. -I ../../ foo -I / usr / include / python3.8 -c qtlib.c -o build / temp.linux-x86_64-3.8 / qtlib.o
gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -march = x86-64 -mtune = generic -O3 -pipe -fno-plt -fno-semantic-interposition -march = x86-64 -mtune = genérico -O3 -pipe -fno-plt -march = x86-64 -mtune = genérico -O3 -pipe -fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected = public -I. -I ../../ foo -I / usr / include / python3.8 -c int_convertors.c -o build / temp.linux-x86_64-3.8 / int_convertors.o
gcc -pthread -Wno-unused-result -Wsign -comparar -DNDEBUG -g -fwrapv -O3 -Wall -march = x86-64 -mtune = genérico -O3 -pipe -fno-plt -fno-semantic-interposition -march = x86-64 -mtune = generic -O3 -pipe -fno-plt -march = x86-64 -mtune = genérico -O3 -pipe -fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected = public -I. -I ../../ foo -I / usr / include / python3.8 -c voidptr.c -o build / temp.linux-x86_64-3.8 / voidptr.o
gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -march = x86-64 -mtune = generic -O3 -pipe -fno-plt -fno-semantic-interposition -march = x86-64 -mtune = genérico -O3 -pipe -fno-plt -march = x86-64 -mtune = genérico -O3 -pipe -fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected = public -I. -I ../../ foo -I / usr / include / python3.8 -c apiversions.c -o build / temp.linux-x86_64-3.8 / apiversions.o
gcc -pthread -Wno-unused-result -Wsign -comparar -DNDEBUG -g -fwrapv -O3 -Wall -march = x86-64 -mtune = genérico -O3 -pipe -fno-plt -fno-semantic-interposition -march = x86-64 -mtune = generic -O3 -pipe -fno-plt -march = x86-64 -mtune = genérico -O3 -pipe -fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected = public -I. -I ../../ foo -I / usr / include / python3.8 -c descriptors.c -o build / temp.linux-x86_64-3.8 / descriptors.o
gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -march = x86-64 -mtune = generic -O3 -pipe -fno-plt -fno-semantic-interposition -march = x86-64 -mtune = genérico -O3 -pipe -fno-plt -march = x86-64 -mtune = genérico -O3 -pipe -fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected = public -I. -I ../../ foo -I / usr / include / python3.8 -c threads.c -o build / temp.linux-x86_64-3.8 / threads.o
gcc -pthread -Wno-unused-result -Wsign -comparar -DNDEBUG -g -fwrapv -O3 -Wall -march = x86-64 -mtune = genérico -O3 -pipe -fno-plt -fno-semantic-interposition -march = x86-64 -mtune = generic -O3 -pipe -fno-plt -march = x86-64 -mtune = genérico -O3 -pipe -fno-plt -fPIC -DSIP_PROTECTED_IS_PUBLIC -Dprotected = public -I. -I ../../ foo -I / usr / include / python3.8 -c siplib.c -o build / temp.linux-x86_64-3.8 / siplib.o
siplib.c: en la función "slot_richcompare":
siplib.c : 9536: 16: advertencia: "st" puede usarse sin inicialización en esta función [-Wmaybe-noinitialized]
9536 | ranura = findSlotInClass (ctd, st);
El | ^ ~~~~~~~~~~~~~~~~~~~~~~~~
siplib.c: 10671: 19: nota: "st" fue declarado aquí
10671 | sipPySlotType st;
El | ^ ~
siplib.c: en la función "parsePass2":
siplib.c: 5625: 32: advertencia: "propietario" se puede utilizar sin inicialización en esta función [-Wmaybe-uninitialized]
5625 | * propietario = arg;
El | ~~~~~~~ ^ ~
g ++ -pthread -shared -Wl, -O1, - sort-common, - según sea necesario, -z, relro, -z, ahora -fno-semántica-interposición -Wl, -O1, - sort-common, --as-needed, -z, relro, -z, ahora build / temp.linux-x86_64-3.8 / sipfoocmodule.o build / temp.linux-x86_64-3.8 / array.o build / temp.linux-x86_64-3.8 /bool.o build / temp.linux-x86_64-3.8 / objmap.o build / temp.linux-x86_64-3.8 / qtlib.o build / temp.linux-x86_64-3.8 / int_convertors.o build / temp.linux-x86_64 -3.8 / voidptr.o build / temp.linux-x86_64-3.8 / apiversions.o build / temp.linux-x86_64-3.8 / descriptors.o build / temp.linux-x86_64-3.8 / threads.o build / temp.linux -x86_64-3.8 / siplib.o -L ../../ foo / bin -L / usr / lib -lfoo -o / home / jenyay / projects / soft / sip-examples / pyfoo_c_01 / build / foo / foo. cpython-38-x86_64-linux-gnu.so
El proyecto ha sido construido.

No profundizaremos en el trabajo de SIP, pero se puede ver en el resultado que algunas fuentes están compilando. Estas fuentes se pueden ver en la carpeta build / foo / creada por este comando :

pyfoo_c_01
├── construir
│ └── foo
│ ├── apiversions.c
│ ├── array.c
│ ├── array.h
│ ├── bool.cpp
│ ├── construir
│ │ └── temp.linux-x86_64-3.8
│ │ ├── apiversions.o
│ │ ├── array.o
│ │ ├── bool.o
│ │ ├── descriptores. O
│ │ ├── int_convertors.o
│ │ ├── objmap.o
│ │ ├── qtlib.o
│ │ ├── sipfoocmodule.o
Ipl │ ├── siplib.o
│ │ ├── hilos.o
│ │ └── voidptr.o
│ ├── descriptors.c
│ ├── foo.cpython-38-x86_64-linux-gnu.so
│ ├── int_convertors.c
│ ├── objmap.c
│ ├── qtlib.c
│ ├── sipAPIfoo.h
│ ├── sipfoocmodule.c
│ ├── sip.h
│ ├── sipint.h
│ ├── siplib.c
│ ├── threads.c
│ └── voidptr.c
├── foo
│ ├── bin
│ │ ├── foo.o
│ │ ├── libfoo.a
│ │ ├── principal
│ │ └── main.o
│ ├── foo.c
│ ├── foo.h
│ ├── main.c
│ └── Makefile
├── pyfoo.sip
└── pyproject.toml


Las fuentes auxiliares aparecieron en la carpeta build / foo . Por curiosidad, veamos el archivo sipfoocmodule.c , ya que se relaciona directamente con el módulo foo que se creará:

/*
 * Module code.
 *
 * Generated by SIP 5.1.1
 */

#include "sipAPIfoo.h"

/* Define the strings used by this module. */
const char sipStrings_foo[] = {
    'f', 'o', 'o', 0,
};

PyDoc_STRVAR(doc_foo, "foo(str) -> int");

static PyObject *func_foo(PyObject *sipSelf,PyObject *sipArgs)
{
    PyObject *sipParseErr = SIP_NULLPTR;

    {
        char* a0;

        if (sipParseArgs(&sipParseErr, sipArgs, "s", &a0))
        {
            int sipRes;

            sipRes = foo(a0);

            return PyLong_FromLong(sipRes);
        }
    }

    /* Raise an exception if the arguments couldn't be parsed. */
    sipNoFunction(sipParseErr, sipName_foo, doc_foo);

    return SIP_NULLPTR;
}

/* This defines this module. */
sipExportedModuleDef sipModuleAPI_foo = {
    0,
    SIP_ABI_MINOR_VERSION,
    sipNameNr_foo,
    0,
    sipStrings_foo,
    SIP_NULLPTR,
    SIP_NULLPTR,
    0,
    SIP_NULLPTR,
    SIP_NULLPTR,
    0,
    SIP_NULLPTR,
    0,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    {SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR,
            SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR, SIP_NULLPTR},
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR
};

/* The SIP API and the APIs of any imported modules. */
const sipAPIDef *sipAPI_foo;

/* The Python module initialisation function. */
#if defined(SIP_STATIC_MODULE)
PyObject *PyInit_foo(void)
#else
PyMODINIT_FUNC PyInit_foo(void)
#endif
{
    static PyMethodDef sip_methods[] = {
        {sipName_foo, func_foo, METH_VARARGS, doc_foo},
        {SIP_NULLPTR, SIP_NULLPTR, 0, SIP_NULLPTR}
    };

    static PyModuleDef sip_module_def = {
        PyModuleDef_HEAD_INIT,
        "foo",
        SIP_NULLPTR,
        -1,
        sip_methods,
        SIP_NULLPTR,
        SIP_NULLPTR,
        SIP_NULLPTR,
        SIP_NULLPTR
    };

    PyObject *sipModule, *sipModuleDict;
    /* Initialise the module and get it's dictionary. */
    if ((sipModule = PyModule_Create(&sip_module_def)) == SIP_NULLPTR)
        return SIP_NULLPTR;

    sipModuleDict = PyModule_GetDict(sipModule);

    if ((sipAPI_foo = sip_init_library(sipModuleDict)) == SIP_NULLPTR)
        return SIP_NULLPTR;

    /* Export the module and publish it's API. */
    if (sipExportModule(&sipModuleAPI_foo, SIP_ABI_MAJOR_VERSION, SIP_ABI_MINOR_VERSION, 0) < 0)
    {
        Py_DECREF(sipModule);
        return SIP_NULLPTR;
    }
    /* Initialise the module now all its dependencies have been set up. */
    if (sipInitModule(&sipModuleAPI_foo,sipModuleDict) < 0)
    {
        Py_DECREF(sipModule);
        return SIP_NULLPTR;
    }

    return sipModule;
}

Si trabajó con la API Python / C, verá funciones familiares. Preste especial atención a la función func_foo que comienza en la línea 18.

Como resultado de la compilación de estas fuentes, se creará el archivo build / foo / foo.cpython-38-x86_64-linux-gnu.so , y contiene la extensión Python, que aún debe instalarse correctamente.

Para compilar la extensión e instalarla de inmediato, puede usar el comando sip-install , pero no lo usaremos, porque de manera predeterminada intenta instalar la extensión Python creada globalmente en el sistema. Este comando tiene un parámetro --target-dir, con el que puede especificar la ruta donde desea instalar la extensión, pero es mejor que usemos otras herramientas que crean paquetes, que luego pueden instalarse utilizando pip.

Primero, use el comando sip-sdist . Usarlo es muy simple:

$ sip-sdist

The sdist has been built.

Después de eso, se creará el archivo pyfoo-0.1.tar.gz , que se puede instalar con el comando:

pip install --user pyfoo-0.1.tar.gz

Como resultado, se mostrará la siguiente información y se instalará el paquete:

Processing ./pyfoo-0.1.tar.gz
  Installing build dependencies ... done
  Getting requirements to build wheel ... done
    Preparing wheel metadata ... done
Building wheels for collected packages: pyfoo
  Building wheel for pyfoo (PEP 517) ... done
  Created wheel for pyfoo: filename=pyfoo-0.1-cp38-cp38-manylinux1_x86_64.whl size=337289 sha256=762fc578...
  Stored in directory: /home/jenyay/.cache/pip/wheels/54/dc/d8/cc534fff...
Successfully built pyfoo
Installing collected packages: pyfoo
  Attempting uninstall: pyfoo
    Found existing installation: pyfoo 0.1
    Uninstalling pyfoo-0.1:
      Successfully uninstalled pyfoo-0.1
Successfully installed pyfoo-0.1

Asegurémonos de que hayamos logrado hacer un enlace de Python. Iniciamos Python e intentamos llamar a la función. Permítame recordarle que de acuerdo con nuestra configuración, el paquete pyfoo contiene el módulo foo , que tiene la función foo .

>>> from foo import foo
>>> foo(b'123456')
12

Tenga en cuenta que, como parámetro de la función, pasamos no solo una cadena, sino una cadena de bytes b'123456 ', un análogo directo de char * a C. Un poco más tarde, agregaremos la conversión de char * a str y viceversa. El resultado era esperado. Permítame recordarle que la función foo devuelve el doble tamaño de una matriz de tipo char * , que se le pasa como parámetro.

Intentemos pasar una cadena Python normal a la función foo en lugar de una lista de bytes.

>>> from foo import foo
>>> foo('123456')

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo(str): argument 1 has unexpected type 'str'

El enlace creado no pudo convertir la cadena a char * ; discutiremos cómo enseñarle cómo hacerlo en la siguiente sección.

Felicitaciones, hicimos el primer enlace en una biblioteca escrita en C.

Salga del intérprete de Python y ensamble el ensamblaje en formato de rueda. Como probablemente sepa, la rueda es un formato de paquete relativamente nuevo que se ha utilizado universalmente recientemente. El formato se describe en PEP 427, "The Wheel Binary Package Format 1.0", pero una descripción de las características del formato de rueda es un tema digno de un artículo grande por separado. Es importante para nosotros que el usuario pueda instalar fácilmente el paquete en formato de rueda utilizando pip.

Un paquete en formato de rueda no es más complicado que un paquete en formato sdist. Para hacer esto, en la carpeta con el archivopyproject.toml necesita ejecutar el comando

sip-wheel

Después de ejecutar este comando, se mostrará el proceso de compilación y puede haber advertencias del compilador:

$ sip-wheel

Estos enlaces se construirán: pyfoo.
Generando los enlaces pyfoo ...
Compilando el módulo 'foo' ...
sipfoocmodule.c: En la función func_foo:
sipfoocmodule.c: 29: 22: advertencia: declaración implícita de la función foo [-Wimplicit-function-declaración]
29 | sipRes = foo (a0);
El | ^ ~~
siplib.c: en la función "slot_richcompare":
siplib.c : 9536: 16: advertencia: "st" se puede utilizar sin inicialización en esta función [-Wmaybe-uninitialized]
9536 | ranura = findSlotInClass (ctd, st);
El | ^ ~~~~~~~~~~~~~~~~~~~~~~~~
siplib.c: 10671: 19: comentario: "st" fue declarado aquí
10671 | sipPySlotType st;
El | ^ ~
siplib.c: en la función "parsePass2":
siplib.c: 5625: 32: advertencia: "propietario" puede utilizarse sin inicialización en esta función [-Wmaybe-noinitialized]
5625 | * propietario = arg;
El | ~~~~~~~ ^ ~ ~
La rueda ha sido construida.

Cuando se complete el ensamblaje (nuestro pequeño proyecto se compila rápidamente), aparecerá un archivo con el nombre pyfoo-0.1-cp38-cp38-manylinux1_x86_64.whl o similar en la carpeta del proyecto . El nombre del archivo generado puede variar según su sistema operativo y la versión de Python.

Ahora podemos instalar este paquete usando pip:

pip install --user --upgrade pyfoo-0.1-cp38-cp38-manylinux1_x86_64.whl

La opción --upgrade se usa aquí para que pip reemplace el módulo pyfoo instalado anteriormente.

Además, se pueden usar el módulo foo y el paquete pyfoo , como se muestra arriba.

Agregar reglas de conversión a char *


En la sección anterior, encontramos un problema de que la función foo solo puede aceptar un conjunto de bytes, pero no cadenas. Ahora arreglaremos esta deficiencia. Para hacer esto, utilizaremos otra herramienta SIP: anotaciones . Las anotaciones se usan dentro de los archivos .sip y se aplican a algunos elementos del código: funciones, clases, argumentos de función, excepciones, variables, etc. Las anotaciones se escriben entre barras diagonales: / annotation / .

Una anotación puede funcionar como un indicador, que puede estar en el estado establecido o no establecido, por ejemplo: / ReleaseGIL / , o algunas anotaciones deben tener asignados algunos valores, por ejemplo: / Encoding = "UTF-8" /. Si es necesario aplicar varias anotaciones a algún objeto, entonces están separadas por comas dentro de barras: / annotation_1, annotation_2 /.

En el siguiente ejemplo, que se encuentra en la carpeta pyfoo_c_02 , agregue un archivo de función de parámetro de anotación pyfoo.sip foo :

%Module(name=foo, language="C")

int foo(char* /Encoding="UTF-8"/);

La anotación de codificación indica en qué codificación se debe codificar la cadena que se pasará a la función. Los valores para esta anotación pueden ser: ASCII, Latin-1, UTF-8 o None. Si la anotación de codificación no se especifica o es igual a Ninguna , entonces el parámetro para dicha función no está sujeto a ninguna codificación y se pasa a la función tal como está, pero en este caso el parámetro en el código Python debe ser de tipo bytes , es decir. una matriz de bytes, como vimos en el ejemplo anterior. Si se especifica la codificación, este parámetro puede ser una cadena (escriba str en Python). La anotación de codificación solo se puede aplicar a parámetros de tipo char , const char ,char * o const char * .

Vamos a ver cómo el foo función del foo módulo funciona ahora . Para hacer esto, como antes, primero debe compilar la biblioteca foo llamando al comando make dentro de la carpeta foo , y luego llame al comando, por ejemplo, sip-wheel, desde la carpeta de ejemplo pyfoo_c_02 . Se creará el archivo pyfoo-0.2-cp38-cp38-manylinux1_x86_64.whl o con un nombre similar, que se puede configurar con el comando:

pip install --user --upgrade pyfoo-0.2-cp38-cp38-manylinux1_x86_64.whl

Si todo salió bien, inicie el intérprete de Python e intente llamar a la función foo con un argumento de cadena:

>>> from foo import foo

>>> foo(b'qwerty')
12

>>> foo('qwerty')
12

>>> foo('')
24

Primero, nos aseguramos de que todavía sea posible usar bytes . Después de eso, nos aseguramos de que ahora también podamos pasar argumentos de cadena a la función foo . Tenga en cuenta que la función foo para un argumento de cadena con letras rusas devuelve un valor dos veces mayor que para una cadena que contiene solo letras latinas. Esto sucedió porque la función foo no cuenta la longitud de la cadena en caracteres (y la duplica), sino la longitud de la matriz char * , y dado que en la codificación UTF-8 las letras rusas ocupan 2 bytes, entonces el tamaño de la matriz char * después de la conversión de Las cadenas de Python resultaron ser el doble de largas.

¡Multa! Resolvimos el problema con el argumento de la función.foo , pero ¿qué pasa si tenemos docenas o cientos de tales funciones en nuestra biblioteca, tendrá que especificar la codificación de parámetros para cada una de ellas? A menudo, la codificación utilizada en un programa es la misma, y ​​no hay ningún propósito para que diferentes funciones indiquen diferentes codificaciones. En este caso, SIP tiene la capacidad de especificar la codificación predeterminada, y si para alguna función la codificación necesita otra, entonces se puede redefinir usando la anotación de codificación .

Para establecer la codificación de los parámetros de la función de forma predeterminada, se utiliza la directiva % DefaultEncoding . Su uso se muestra en el ejemplo ubicado en la carpeta pyfoo_c_03 .

Para aprovechar la directiva % DefaultEncoding , cambie el archivo pyfoo.sip, ahora su contenido es el siguiente:

%Module(name=foo, language="C")
%DefaultEncoding "UTF-8"

int foo(char*);

Ahora, si el argumento es una función de tipo char , char * , etc. Si no hay una anotación de codificación , la codificación se toma de la directiva % DefaultEncoding , y si no es así, la conversión no se realiza, y para todos los parámetros char * , etc. es necesario transferir no líneas sino bytes .

Un ejemplo de la carpeta pyfoo_c_03 se recopila y verifica de la misma manera que un ejemplo de la carpeta pyfoo_c_02 .

Brevemente sobre project.py. Automatizar el montaje


Hasta ahora, hemos utilizado dos archivos de utilidades para crear un enlace de Python: pyproject.toml y pyfoo.sip . Ahora nos familiarizaremos con otro archivo de este tipo, que debería llamarse project.py . Con este script, podemos influir en el proceso de compilación de nuestro paquete. Hagamos la automatización de compilación. Para recopilar los ejemplos pyfoo_c_01 - pyfoo_c_03 de las secciones anteriores, primero tenía que ir a la carpeta foo / , compilar allí usando el comando make , volver a la carpeta donde se encuentra el archivo pyproject.toml y solo luego comenzar a construir el paquete usando uno de sip- * comandos .

Ahora nuestro objetivo es asegurarnos de que al ejecutar los comandos sip-build , sip-sdist y sip-wheel , el ensamblaje de foo C-library se inicie primero , y luego el comando en sí ya se haya iniciado.

Un ejemplo creado en esta sección se encuentra en la carpeta de origen pyfoo_c_04 .

Para cambiar el proceso de compilación, podemos declarar una clase en el archivo project.py (el nombre del archivo debe ser solo eso) que se deriva de la clase sipbuild.Project . Esta clase tiene métodos que podemos anular por nuestra cuenta. Actualmente estamos interesados ​​en los siguientes métodos:

  • construir . Llamado durante la llamada al comando sip-build .
  • build_sdist . Se llama cuando se llama al comando sip-sdist .
  • build_wheel . Se llama cuando se llama al comando sip-wheel .
  • instalar . Se invoca cuando se invoca el comando sip-install .

Es decir, podemos redefinir el comportamiento de estos comandos. Estrictamente hablando, los métodos enumerados se declaran en la clase abstracta sipbuild.AbstractProject , a partir de la cual se crea la clase derivada sipbuild.Project .

Cree un archivo project.py con los siguientes contenidos:

import os
import subprocess

from sipbuild import Project

class FooProject(Project):
    def _build_foo(self):
        cwd = os.path.abspath('foo')
        subprocess.run(['make'], cwd=cwd, capture_output=True, check=True)

    def build(self):
        self._build_foo()
        super().build()

    def build_sdist(self, sdist_directory):
        self._build_foo()
        return super().build_sdist(sdist_directory)

    def build_wheel(self, wheel_directory):
        self._build_foo()
        return super().build_wheel(wheel_directory)

    def install(self):
        self._build_foo()
        super().install()

Declaramos la clase FooProject , derivada de la clase sipbuild.Project, y definimos los métodos build , build_sdist , build_wheel e instalación en ella . En todos estos métodos, llamamos a los métodos con el mismo nombre de la clase base, antes de llamar al método _build_foo , que inicia la ejecución del comando make en la carpeta foo .

Tenga en cuenta que los métodos build_sdist y build_wheel deberían devolver el nombre del archivo que crearon. Esto no está escrito en la documentación, pero está indicado en las fuentes SIP.

Ahora no necesitamos ejecutar el comando makemanualmente para construir la biblioteca foo , esto se hará automáticamente.

Si ahora ejecuta el comando sip-wheel en la carpeta pyfoo_c_04 , se crea un archivo con el nombre pyfoo-0.4-cp38-cp38-manylinux1_x86_64.whl o similar, dependiendo de su sistema operativo y versión de Python. Este paquete se puede instalar usando el comando:



pip install --user --upgrade pyfoo-0.4-cp38-cp38-manylinux1_x86_64.whl

Después de eso, puede asegurarse de que el foo función del foo módulo sigue funcionando.

Agregar opciones de línea de comando para construir


El siguiente ejemplo se encuentra en la carpeta pyfoo_c_05 , y el paquete tiene un número de versión de 0.5 (consulte la configuración en el archivo pyproject.toml ). Este ejemplo se basa en un ejemplo de la documentación con algunas correcciones. En este ejemplo, volveremos a hacer nuestro archivo project.py y agregaremos nuevas opciones de línea de comandos para la compilación.

En nuestros ejemplos, estamos construyendo una biblioteca muy simple foo, y en proyectos reales, la biblioteca puede ser bastante grande y no tendrá sentido incluirla en el código fuente del proyecto de enlace de Python. Permítame recordarle que SIP se creó originalmente para crear un enlace para una biblioteca tan grande como Qt. Por supuesto, puede argumentar que los submódulos de git pueden ayudar a organizar la fuente, pero ese no es el punto. Suponga que la biblioteca puede no estar en la carpeta con la fuente de enlace. En este caso, surge la pregunta: ¿dónde debe buscar el recopilador SIP el encabezado de la biblioteca y los archivos de objeto? En este caso, diferentes usuarios pueden tener sus propias formas de colocar la biblioteca.

Para resolver este problema, agregaremos dos nuevas opciones de línea de comandos al sistema de compilación, con el que puede especificar la ruta al archivo foo.h (parámetro --foo-include-dir) y al archivo de objeto de biblioteca (parámetro --foo-library-dir ). Además, asumiremos que si no se especifican estos parámetros, la biblioteca foo todavía se encuentra junto con las fuentes de enlace.

Necesitamos crear el archivo project.py nuevamente , y en él declarar una clase derivada de sipbuild.Project . Veamos primero la nueva versión del archivo project.py y luego veamos cómo funciona.

import os

from sipbuild import Option, Project

class FooProject(Project):
    """        
       foo.
    """

    def get_options(self):
        """     . """

        tools = ['build', 'install', 'sdist', 'wheel']

        #   .
        options = super().get_options()

        #   
        inc_dir_option = Option('foo_include_dir',
                                help="the directory containing foo.h",
                                metavar="DIR",
                                default=os.path.abspath('foo'),
                                tools=tools)
        options.append(inc_dir_option)

        lib_dir_option = Option('foo_library_dir',
                                help="the directory containing the foo library",
                                metavar="DIR",
                                default=os.path.abspath('foo/bin'),
                                tools=tools)

        options.append(lib_dir_option)

        return options

    def apply_user_defaults(self, tool):
        """    . """

        #     
        super().apply_user_defaults(tool)

        #  ,         
        self.foo_include_dir = os.path.abspath(self.foo_include_dir)
        self.foo_library_dir = os.path.abspath(self.foo_library_dir)

    def update(self, tool):
        """   . """

        #   pyfoo
        # (  pyproject.toml  [tool.sip.bindings.pyfoo])
        foo_bindings = self.bindings['pyfoo']

        #   include_dirs  
        if self.foo_include_dir is not None:
            foo_bindings.include_dirs = [self.foo_include_dir]

        #   library_dirs  
        if self.foo_library_dir is not None:
            foo_bindings.library_dirs = [self.foo_library_dir]

        super().update(tool)

Nuevamente creamos la clase FooProject , derivada de sipbuild.Project . En este ejemplo, el ensamblaje automático de la biblioteca foo está deshabilitado , porque ahora se supone que puede estar en otro lugar, y para cuando se cree el enlace, los archivos de encabezado y objeto deberían estar listos. Tres métodos se redefinen

en el FooProject clase : get_options , apply_user_defaults, y actualización . Considérelos con más cuidado.

Comencemos con el método get_options . Este método debería devolver una lista de instancias de la clase sipbuild.Option. Cada elemento de la lista es una opción de línea de comando. Dentro del método anulado, obtenemos la lista de opciones predeterminadas (la variable de opciones ) llamando al método de clase base del mismo nombre, luego creamos dos nuevas opciones ( --foo_include_dir y --foo_library_dir ) y las agregamos a la lista, y luego devolvemos esta lista de la función.

El constructor de la clase Opción acepta un parámetro requerido (nombre de la opción) y un número suficientemente grande de opcionales que describen el tipo de valor para este parámetro, el valor predeterminado, la descripción del parámetro y algunos otros. Este ejemplo utiliza las siguientes opciones para el constructor de opciones :

  • help , , sip-wheel -h
  • metavar — , , . metavar «DIR», , — .
  • default — . , , foo , ( ).
  • tools — , . sip-build, sip-install, sip-sdist sip-wheel, tools = ['build', 'install', 'sdist', 'wheel'].

El siguiente método sobrecargado apply_user_defaults está diseñado para establecer valores de parámetros que el usuario puede pasar a través de la línea de comando. El método apply_user_defaults de la clase base crea una variable (miembro de clase) para cada parámetro de línea de comando creado en el método get_options , por lo que es importante llamar al método de clase base del mismo nombre antes de usar las variables creadas para que todas las variables creadas usando los parámetros de línea de comando se creen e inicialicen con valores predeterminados . Después de eso, en nuestro ejemplo, se crearán las variables self.foo_include_dir y self.foo_library_dir. Si el usuario no ha especificado los parámetros de línea de comando correspondientes, tomará los valores predeterminados de acuerdo con los parámetros del constructor de la clase Opción (parámetro predeterminado ). Si no se establece el parámetro predeterminado , dependiendo del tipo del valor del parámetro esperado, se inicializará Ninguno, o una lista vacía, o 0.

Dentro del método apply_user_defaults , nos aseguramos de que las rutas en las variables self.foo_include_dir y self.foo_library_dir sean siempre absolutas. Esto es necesario para que no dependa de cuál será la carpeta de trabajo en el momento en que se inicie el ensamblaje.

El último método sobrecargado en esta clase es la actualización.. Se llama a este método cuando es necesario aplicar al proyecto los cambios realizados antes. Por ejemplo, cambie o agregue los parámetros especificados en el archivo pyproject.toml . En los ejemplos anteriores, establecemos las rutas a los archivos de encabezado y objeto utilizando los parámetros include-dirs y library-dirs , respectivamente, dentro de la sección [tool.sip.bindings.pyfoo] . Ahora estableceremos estos parámetros desde el script project.py , por lo que en el archivo pyproject.toml eliminaremos estos parámetros:

[build-system]
requires = ["sip >=5, <6"]
build-backend = "sipbuild.api"

[tool.sip.metadata]
name = "pyfoo"
version = "0.3"
license = "MIT"

[tool.sip.bindings.pyfoo]
headers = ["foo.h"]
libraries = ["foo"]

Dentro del método, actualícenos desde el diccionario self.bindings keyed pyfoo gat instance sipbuild.Bindings . El nombre de la clave corresponde a [tool.sip.bindings. pyfoo ] del archivo pyproject.toml , y la instancia de clase así obtenida describe la configuración descrita en esta sección. Luego, los miembros de esta clase include_dirs y library_dirs (los nombres de los miembros corresponden a los parámetros include-dirs y library-dirs con un guión reemplazado por guiones bajos) se les asignan listas que contienen las rutas almacenadas en los miembros self.foo_include_dir y self.foo_library_dir. En este ejemplo, para mayor precisión, verificamos que los valores self.foo_include_dir y self.foo_library_dir no son iguales a None , pero en este ejemplo esta condición siempre se cumple porque los parámetros de la línea de comandos que creamos tienen valores predeterminados.

Por lo tanto, preparamos los archivos de configuración para que durante el ensamblaje fuera posible indicar las rutas a los archivos de encabezado y objeto. Veamos qué pasó.

Primero, asegúrese de que los valores predeterminados funcionen. Para hacer esto, vaya a la carpeta pyfoo_c_05 / foo y compile la biblioteca usando el comando make , porque en este ejemplo deshabilitamos la compilación automática de la biblioteca.

Después de eso, ve a la carpetapyfoo_c_05 y ejecuta el comando sip-wheel . Como resultado de este comando, se creará el archivo pyfoo-0.5-cp38-cp38-manylinux1_x86_64.whl o con un nombre similar.

Ahora mueva la carpeta foo en algún lugar fuera de la carpeta pyfoo_c_05 y ejecute el comando sip-wheel nuevamente . Como resultado, obtenemos el error esperado informando que no tenemos un archivo de biblioteca de objetos:

usr/bin/ld:   -lfoo
collect2: :  ld     1
sip-wheel: Unable to compile the 'foo' module: command 'g++' failed with exit status 1

Después de eso, ejecute sip-wheel con la nueva opción de línea de comandos:

sip-wheel --foo-include-dir ".../foo" --foo-library-dir ".../foo/bin"

En lugar de los puntos suspensivos, debe especificar la ruta a la carpeta donde movió la carpeta foo con la biblioteca ensamblada. Como resultado, el ensamblado debería tener éxito en la creación del archivo .whl. El módulo creado se puede instalar y probar de la misma manera que en las secciones anteriores.

Verifique el orden de los métodos de llamada desde project.py


El siguiente ejemplo, que consideraremos, será muy simple; demostrará el orden de invocación de los métodos de la clase Project , que hemos sobrecargado en las secciones anteriores. Esto puede ser útil para comprender cuándo se pueden inicializar las variables. Este ejemplo se encuentra en la carpeta pyfoo_c_06 en el repositorio de origen.

La esencia de este ejemplo es sobrecargar todos los métodos que usamos antes en la clase FooProject , que se encuentra en el archivo project.py , y agregarles llamadas a la función de impresión , que mostrarían el nombre del método en el que se encuentra:

from sipbuild import Project

class FooProject(Project):
    def get_options(self):
        print('get_options()')
        options = super().get_options()
        return options

    def apply_user_defaults(self, tool):
        print('apply_user_defaults()')
        super().apply_user_defaults(tool)

    def apply_nonuser_defaults(self, tool):
        print('apply_nonuser_defaults()')
        super().apply_nonuser_defaults(tool)

    def update(self, tool):
        print('update()')
        super().update(tool)

    def build(self):
        print('build()')
        super().build()

    def build_sdist(self, sdist_directory):
        print('build_sdist()')
        return super().build_sdist(sdist_directory)

    def build_wheel(self, wheel_directory):
        print('build_wheel()')
        return super().build_wheel(wheel_directory)

    def install(self):
        print('install()')
        super().install()

Los lectores atentos deben tener en cuenta que, además de los métodos utilizados anteriormente, el método apply_nonuser_defaults () , del que no hemos hablado antes , está sobrecargado en este ejemplo . Este método recomienda establecer valores predeterminados para todas las variables que no se pueden cambiar a través de parámetros de línea de comandos.

En el archivo pyproject.toml, devuelva la ruta explícita a la biblioteca:

[build-system]
requires = ["sip >=5, <6"]
build-backend = "sipbuild.api"

[tool.sip.metadata]
name = "pyfoo"
version = "0.4"
license = "MIT"

[tool.sip.bindings.pyfoo]
headers = ["foo.h"]
libraries = ["foo"]
include-dirs = ["foo"]
library-dirs = ["foo/bin"]

Para que el proyecto se compile correctamente, debe ir a la carpeta foo y construir la biblioteca allí con el comando make . Después de eso, regrese a la carpeta pyfoo_c_06 y ejecute, por ejemplo, el comando sip-wheel . Como resultado, si descarta las advertencias del compilador, se mostrará el siguiente texto:

get_options ()
apply_nonuser_defaults ()
get_options ()
get_options ()
apply_user_defaults ()
get_options ()
update ()
Estos enlaces se construirán: pyfoo.
build_wheel ()
Generando los enlaces pyfoo ...
Compilando el módulo 'foo' ...
La rueda ha sido construida.

Se muestran líneas en negrita que salen de nuestro archivo project.py . Por lo tanto, vemos que el método get_options se llama varias veces, y esto debe tenerse en cuenta si va a inicializar cualquier variable miembro en la clase que se deriva de Project . El método get_options no es el mejor lugar para esto.

También es útil recordar que el método apply_nonuser_defaults se llama antes que el método apply_user_defaults , es decir en el método apply_user_defaults ya es posible usar variables cuyos valores se establecen en el método apply_nonuser_defaults .

Después de eso, el método de actualización se llama, y al final, el método directamente responsable del ensamblaje, en nuestro caso, build_wheel .

Conclusión de la primera parte.


En este artículo, comenzamos a estudiar la herramienta SIP diseñada para crear enlaces de Python para bibliotecas escritas en C o C ++. En esta primera parte del artículo, examinamos los conceptos básicos del uso de SIP utilizando el ejemplo de crear un enlace de Python para una biblioteca muy simple escrita en C.

Descubrimos los archivos que necesita crear para trabajar con SIP. El archivo pyproject.toml contiene información sobre el paquete (nombre, número de versión, licencia y rutas a los archivos de encabezado y objeto). Usando el archivo project.py , puede influir en el proceso de compilación del paquete Python, por ejemplo, comenzar a construir la biblioteca C o dejar que el usuario especifique la ubicación de los archivos de encabezado y objeto de la biblioteca.

En el archivo * .sipdescribe la interfaz del módulo de Python que enumera las funciones y clases que se incluirán en el módulo. Las directivas y anotaciones se usan para describir la interfaz en el archivo * .sip .

En la segunda parte del artículo, crearemos un enlace sobre una biblioteca orientada a objetos escrita en C ++, y con su ejemplo estudiaremos técnicas que serán útiles para describir la interfaz de las clases de C ++, y al mismo tiempo trataremos con nuevas directivas y anotaciones para nosotros.

Continuará.

Referencias



All Articles