Membuat binding Python untuk pustaka C / C ++ menggunakan SIP. Bagian 2

Di bagian pertama artikel, kami memeriksa dasar-dasar bekerja dengan utilitas SIP yang dirancang untuk membuat binding Python untuk perpustakaan yang ditulis dalam C dan C ++. Kami melihat file dasar yang perlu Anda buat agar berfungsi dengan SIP dan mulai melihat arahan dan anotasi. Sejauh ini, kami telah melakukan pengikatan untuk pustaka sederhana yang ditulis dalam C. Pada bagian ini, kami akan mencari tahu bagaimana melakukan pengikatan untuk pustaka C ++ yang berisi kelas. Menggunakan contoh perpustakaan ini, kita akan melihat teknik apa yang bisa berguna ketika bekerja dengan perpustakaan berorientasi objek, dan pada saat yang sama kita akan berurusan dengan arahan dan penjelasan baru untuk kita.

Semua contoh untuk artikel tersedia di repositori github di: https://github.com/Jenyay/sip-examples .

Membuat pengikatan untuk perpustakaan di C ++


Contoh berikut, yang akan kami pertimbangkan, ada di folder pyfoo_cpp_01 .

Pertama, buat pustaka tempat kita akan melakukan penjilidan. Perpustakaan akan tetap berada di folder foo dan berisi satu kelas - Foo . File header foo.h dengan deklarasi kelas ini adalah sebagai berikut:

#ifndef FOO_LIB
#define FOO_LIB

class Foo {
    private:
        int _int_val;
        char* _string_val;
    public:
        Foo(int int_val, const char* string_val);
        virtual ~Foo();

        void set_int_val(int val);
        int get_int_val();

        void set_string_val(const char* val);
        char* get_string_val();
};

#endif

Ini adalah kelas sederhana dengan dua getter dan setter yang menetapkan dan mengembalikan nilai dari tipe int dan char * . Implementasi kelas adalah sebagai berikut:

#include <string.h>

#include "foo.h"

Foo::Foo(int int_val, const char* string_val): _int_val(int_val) {
    _string_val = nullptr;
    set_string_val(string_val);
}

Foo::~Foo(){
    delete[] _string_val;
    _string_val = nullptr;
}

void Foo::set_int_val(int val) {
    _int_val = val;
}

int Foo::get_int_val() {
    return _int_val;
}

void Foo::set_string_val(const char* val) {
    if (_string_val != nullptr) {
        delete[] _string_val;
    }

    auto count = strlen(val) + 1;
    _string_val = new char[count];
    strcpy(_string_val, val);
}

char* Foo::get_string_val() {
    return _string_val;
}

Untuk menguji fungsionalitas perpustakaan, folder foo juga berisi file main.cpp menggunakan kelas Foo :

#include <iostream>

#include "foo.h"

using std::cout;
using std::endl;

int main(int argc, char* argv[]) {
    auto foo = Foo(10, "Hello");
    cout << "int_val: " << foo.get_int_val() << endl;
    cout << "string_val: " << foo.get_string_val() << endl;

    foo.set_int_val(0);
    foo.set_string_val("Hello world!");

    cout << "int_val: " << foo.get_int_val() << endl;
    cout << "string_val: " << foo.get_string_val() << endl;
}

Untuk membangun perpustakaan foo , gunakan Makefile berikut :

CC=g++
CFLAGS=-c -fPIC
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.cpp
    $(CC) $(CFLAGS) main.cpp -o $(DIR_OUT)/main.o

libfoo.a: makedir foo.cpp
    $(CC) $(CFLAGS) foo.cpp -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)/*

Perbedaan dari Makefile pada contoh sebelumnya, selain mengubah kompiler dari gcc ke g ++ , adalah bahwa opsi lain -fPIC ditambahkan untuk kompilasi , yang memberi tahu kompiler untuk menempatkan kode di perpustakaan dengan cara tertentu (yang disebut "kode posisi-independen"). Karena artikel ini bukan tentang kompiler, kami tidak akan memeriksa secara lebih terperinci apa yang parameter ini lakukan dan mengapa dibutuhkan.

Mari mulai mengikat perpustakaan ini. File pyproject.toml dan project.py hampir tidak berubah dari contoh sebelumnya. Inilah yang terlihat seperti file pyproject.toml sekarang :

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

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

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

Sekarang contoh kita yang ditulis dalam C ++ akan dikemas dalam paket pyfoocpp Python , ini mungkin satu-satunya perubahan nyata dalam file ini.

File project.py tetap sama seperti pada contoh pyfoo_c_04 :

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()

Dan di sini kita akan mempertimbangkan file pyfoocpp.sip lebih terinci. Biarkan saya mengingatkan Anda bahwa file ini menjelaskan antarmuka untuk modul Python masa depan: apa yang harus termasuk, seperti apa antarmuka kelas, dll. File .sip tidak diperlukan untuk mengulang file header perpustakaan, meskipun mereka memiliki banyak kesamaan. Di dalam kelas ini, metode baru dapat ditambahkan yang tidak ada di kelas asli. Itu antarmuka yang dijelaskan dalam file .sip dapat menyesuaikan kelas perpustakaan dengan prinsip-prinsip yang diterima dengan Python, jika perlu. Dalam file pyfoocpp.sip kita akan melihat arahan baru untuk kita.

Pertama, mari kita lihat apa isi file ini:

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

class Foo {
    %TypeHeaderCode
    #include <foo.h>
    %End

    public:
        Foo(int, const char*);

        void set_int_val(int);
        int get_int_val();

        void set_string_val(const char*);
        char* get_string_val();
};

Baris pertama seharusnya sudah jelas bagi kita dari contoh sebelumnya. Dalam direktif % Module , kami menunjukkan nama modul Python yang akan dibuat (yaitu, untuk menggunakan modul ini, kita perlu menggunakan perintah import foocpp atau dari foocpp import ... Dalam direktif yang sama, kami mengindikasikan bahwa kami sekarang memiliki bahasa - C ++. % DefaultEncoding direktif menetapkan pengkodean yang akan digunakan untuk mengubah string Python menjadi char , const char , char * dan const char * types .

Kemudian deklarasi antarmuka kelas Foo mengikuti . Segera setelah deklarasi kelas Foomasih belum menggunakan direktif % TypeHeaderCode , yang diakhiri dengan direktif % End . The TypeHeaderCode direktif% harus berisi kode yang menyatakan antarmuka dari C ++ class yang bungkusnya sedang dibuat. Sebagai aturan, dalam arahan ini cukup untuk menyertakan file header dengan deklarasi kelas.

Setelah itu, metode kelas yang akan dikonversi ke metode kelas Foo untuk bahasa Python terdaftar . Penting untuk dicatat bahwa saat ini kami hanya mendeklarasikan metode publik yang akan dapat diakses dari kelas Foo dengan Python (karena tidak ada anggota pribadi dan yang dilindungi dengan Python). Karena kami menggunakan arahan % DefaultEncoding di awal, kemudian dalam metode yang mengambil argumen tipe const * , Anda tidak dapat menggunakan anotasi Pengkodean untuk menentukan pengodean untuk mengubah parameter ini ke string Python dan sebaliknya.

Sekarang kita hanya perlu mengkompilasi paket python pyfoocpp dan mengujinya. Tetapi sebelum merakit paket lengkap, mari kita gunakan perintah sip-build dan lihat file sumber apa yang akan dibuat oleh SIP untuk kompilasi nanti, dan coba cari di dalamnya sesuatu yang mirip dengan kelas yang akan dibuat dalam kode Python. Untuk melakukan ini, perintah sip-build di atas harus dipanggil dalam folder pyfoo_cpp_01 . Akibatnya, folder build akan dibuat. dengan konten berikut:

membangun
└── foocpp
    β”œβ”€β”€ apiversions.c
    β”œβ”€β”€ array.c
    β”œβ”€β”€ array.h
    β”œβ”€β”€ bool.cpp
    β”œβ”€β”€ build
    β”‚   └── temp.linux-x86_64-3.8
    β”‚       β”œβ”€β”€ apiversions.o
    β”‚       β”œβ”€β”€ array.o
    β”‚       β”œβ”€β”€ bool.o
    β”‚       β”œβ”€β”€ descriptors.o
    β”‚       β”œβ”€β”€ int_convertors.o
    β”‚       β”œβ”€β”€ objmap.o
    β”‚       β”œβ”€β”€ qtlib.o
    β”‚       β”œβ”€β”€ sipfoocppcmodule.o
    β”‚       β”œβ”€β”€ sipfoocppFoo.o
    β”‚       β”œβ”€β”€ siplib.o
    β”‚       β”œβ”€β”€ threads.o
    β”‚       └── voidptr.o
    β”œβ”€β”€ descriptors.c
    β”œβ”€β”€ foocpp.cpython-38-x86_64-linux-gnu.so
    β”œβ”€β”€ int_convertors.c
    β”œβ”€β”€ objmap.c
    β”œβ”€β”€ qtlib.c
    β”œβ”€β”€ sipAPIfoocpp.h
    β”œβ”€β”€ sipfoocppcmodule.cpp
    β”œβ”€β”€ sipfoocppFoo.cpp
    β”œβ”€β”€ sip.h
    β”œβ”€β”€ sipint.h
    β”œβ”€β”€ siplib.c
    β”œβ”€β”€ threads.c
    └── voidptr.c


Sebagai tugas tambahan, pertimbangkan file sipfoocppFoo.cpp dengan hati-hati (kami tidak akan membahasnya secara rinci dalam artikel ini):

/*
 * Interface wrapper code.
 *
 * Generated by SIP 5.1.1
 */

#include "sipAPIfoocpp.h"

#line 6 "/home/jenyay/temp/2/pyfoocpp.sip"
    #include <foo.h>
#line 12 "/home/jenyay/temp/2/build/foocpp/sipfoocppFoo.cpp"

PyDoc_STRVAR(doc_Foo_set_int_val, "set_int_val(self, int)");

extern "C" {static PyObject *meth_Foo_set_int_val(PyObject *, PyObject *);}
static PyObject *meth_Foo_set_int_val(PyObject *sipSelf, PyObject *sipArgs)
{
    PyObject *sipParseErr = SIP_NULLPTR;

    {
        int a0;
         ::Foo *sipCpp;

        if (sipParseArgs(&sipParseErr, sipArgs, "Bi", &sipSelf, sipType_Foo, &sipCpp, &a0))
        {
            sipCpp->set_int_val(a0);

            Py_INCREF(Py_None);
            return Py_None;
        }
    }

    /* Raise an exception if the arguments couldn't be parsed. */
    sipNoMethod(sipParseErr, sipName_Foo, sipName_set_int_val, doc_Foo_set_int_val);

    return SIP_NULLPTR;
}

PyDoc_STRVAR(doc_Foo_get_int_val, "get_int_val(self) -> int");

extern "C" {static PyObject *meth_Foo_get_int_val(PyObject *, PyObject *);}
static PyObject *meth_Foo_get_int_val(PyObject *sipSelf, PyObject *sipArgs)
{
    PyObject *sipParseErr = SIP_NULLPTR;

    {
         ::Foo *sipCpp;

        if (sipParseArgs(&sipParseErr, sipArgs, "B", &sipSelf, sipType_Foo, &sipCpp))
        {
            int sipRes;

            sipRes = sipCpp->get_int_val();

            return PyLong_FromLong(sipRes);
        }
    }

    /* Raise an exception if the arguments couldn't be parsed. */
    sipNoMethod(sipParseErr, sipName_Foo, sipName_get_int_val, doc_Foo_get_int_val);

    return SIP_NULLPTR;
}

PyDoc_STRVAR(doc_Foo_set_string_val, "set_string_val(self, str)");

extern "C" {static PyObject *meth_Foo_set_string_val(PyObject *, PyObject *);}
static PyObject *meth_Foo_set_string_val(PyObject *sipSelf, PyObject *sipArgs)
{
    PyObject *sipParseErr = SIP_NULLPTR;

    {
        const char* a0;
        PyObject *a0Keep;
         ::Foo *sipCpp;

        if (sipParseArgs(&sipParseErr, sipArgs, "BA8", &sipSelf, sipType_Foo, &sipCpp, &a0Keep, &a0))
        {
            sipCpp->set_string_val(a0);
            Py_DECREF(a0Keep);

            Py_INCREF(Py_None);
            return Py_None;
        }
    }

    /* Raise an exception if the arguments couldn't be parsed. */
    sipNoMethod(sipParseErr, sipName_Foo, sipName_set_string_val, doc_Foo_set_string_val);

    return SIP_NULLPTR;
}

PyDoc_STRVAR(doc_Foo_get_string_val, "get_string_val(self) -> str");

extern "C" {static PyObject *meth_Foo_get_string_val(PyObject *, PyObject *);}
static PyObject *meth_Foo_get_string_val(PyObject *sipSelf, PyObject *sipArgs)
{
    PyObject *sipParseErr = SIP_NULLPTR;

    {
         ::Foo *sipCpp;

        if (sipParseArgs(&sipParseErr, sipArgs, "B", &sipSelf, sipType_Foo, &sipCpp))
        {
            char*sipRes;

            sipRes = sipCpp->get_string_val();

            if (sipRes == SIP_NULLPTR)
            {
                Py_INCREF(Py_None);
                return Py_None;
            }

            return PyUnicode_FromString(sipRes);
        }
    }

    /* Raise an exception if the arguments couldn't be parsed. */
    sipNoMethod(sipParseErr, sipName_Foo, sipName_get_string_val, doc_Foo_get_string_val);

    return SIP_NULLPTR;
}

/* Call the instance's destructor. */
extern "C" {static void release_Foo(void *, int);}
static void release_Foo(void *sipCppV, int)
{
    delete reinterpret_cast< ::Foo *>(sipCppV);
}

extern "C" {static void dealloc_Foo(sipSimpleWrapper *);}
static void dealloc_Foo(sipSimpleWrapper *sipSelf)
{
    if (sipIsOwnedByPython(sipSelf))
    {
        release_Foo(sipGetAddress(sipSelf), 0);
    }
}

extern "C" {static void *init_type_Foo(sipSimpleWrapper *, PyObject *, 
                 PyObject *, PyObject **, PyObject **, PyObject **);}
static void *init_type_Foo(sipSimpleWrapper *, PyObject *sipArgs, PyObject *sipKwds,
                                   PyObject **sipUnused, PyObject **, PyObject **sipParseErr)
{
     ::Foo *sipCpp = SIP_NULLPTR;

    {
        int a0;
        const char* a1;
        PyObject *a1Keep;

        if (sipParseKwdArgs(sipParseErr, sipArgs, sipKwds, SIP_NULLPTR, sipUnused, "iA8", &a0, &a1Keep, &a1))
        {
            sipCpp = new  ::Foo(a0,a1);
            Py_DECREF(a1Keep);

            return sipCpp;
        }
    }

    {
        const  ::Foo* a0;

        if (sipParseKwdArgs(sipParseErr, sipArgs, sipKwds, SIP_NULLPTR, sipUnused, "J9", sipType_Foo, &a0))
        {
            sipCpp = new  ::Foo(*a0);

            return sipCpp;
        }
    }

    return SIP_NULLPTR;
}

static PyMethodDef methods_Foo[] = {
    {sipName_get_int_val, meth_Foo_get_int_val, METH_VARARGS, doc_Foo_get_int_val},
    {sipName_get_string_val, meth_Foo_get_string_val, METH_VARARGS, doc_Foo_get_string_val},
    {sipName_set_int_val, meth_Foo_set_int_val, METH_VARARGS, doc_Foo_set_int_val},
    {sipName_set_string_val, meth_Foo_set_string_val, METH_VARARGS, doc_Foo_set_string_val}
};

PyDoc_STRVAR(doc_Foo, "\1Foo(int, str)\n"
"Foo(Foo)");

sipClassTypeDef sipTypeDef_foocpp_Foo = {
    {
        -1,
        SIP_NULLPTR,
        SIP_NULLPTR,
        SIP_TYPE_CLASS,
        sipNameNr_Foo,
        SIP_NULLPTR,
        SIP_NULLPTR
    },
    {
        sipNameNr_Foo,
        {0, 0, 1},
        4, methods_Foo,
        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},
    },
    doc_Foo,
    -1,
    -1,
    SIP_NULLPTR,
    SIP_NULLPTR,
    init_type_Foo,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    dealloc_Foo,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    release_Foo,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR,
    SIP_NULLPTR
};

Sekarang bangun paket menggunakan perintah sip-wheel . Setelah menjalankan perintah ini, jika semuanya berjalan dengan baik, file pyfoocpp-0.1-cp38-cp38-manylinux1_x86_64.whl atau dengan nama yang sama akan dibuat . Instal menggunakan pip install --user pyfoocpp-0.1-cp38-cp38-manylinux1_x86_64.whl dan jalankan interpreter Python untuk memverifikasi:

>>> from foocpp import Foo
>>> x = Foo(10, 'Hello')

>>> x.get_int_val()
10

>>> x.get_string_val()
'Hello'

>>> x.set_int_val(50)
>>> x.set_string_val('')

>>> x.get_int_val()
50

>>> x.get_string_val()
''

Bekerja! Jadi, kami baru saja membuat modul Python dengan pengikatan untuk kelas di C ++. Selanjutnya kami akan membawa keindahan ke kelas ini dan menambahkan berbagai fasilitas.

Tambahkan properti


Kelas yang dibuat menggunakan SIP tidak diperlukan untuk mengulangi antarmuka kelas C ++. Misalnya, di kelas Foo kami, ada dua getter dan dua setter, yang dapat dengan jelas digabungkan menjadi properti untuk membuat kelas lebih "Python". Menambahkan properti menggunakan sip cukup mudah seperti yang dilakukan, contoh di folder pyfoo_cpp_02 menunjukkan .

Contoh ini mirip dengan yang sebelumnya, perbedaan utama adalah pada file pyfoocpp.sip , yang sekarang terlihat seperti ini:

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

class Foo {
    %TypeHeaderCode
    #include <foo.h>
    %End

    public:
        Foo(int, const char*);

        void set_int_val(int);
        int get_int_val();
        %Property(name=int_val, get=get_int_val, set=set_int_val)

        void set_string_val(const char*);
        char* get_string_val();
        %Property(name=string_val, get=get_string_val, set=set_string_val)
};

Seperti yang Anda lihat, semuanya cukup sederhana. Untuk menambahkan properti, direktif % Properti dimaksudkan , yang memiliki dua parameter yang diperlukan: nama untuk menentukan nama properti, dan bisa menentukan metode yang mengembalikan nilai (pengambil). Mungkin tidak ada setter, tetapi jika properti juga perlu diberi nilai, metode setter ditentukan sebagai nilai parameter yang ditetapkan . Dalam contoh kami, properti dibuat dengan cara yang cukup mudah, karena sudah ada fungsi yang berfungsi sebagai getter dan setter.

Kami hanya dapat mengumpulkan paket menggunakan perintah sip-wheel , instal, setelah itu kami akan memeriksa operasi properti dalam mode perintah interpreter python:

>>> from foocpp import Foo
>>> x = Foo(10, "Hello")
>>> x.int_val
10

>>> x.string_val
'Hello'

>>> x.int_val = 50
>>> x.string_val = ''

>>> x.get_int_val()
50

>>> x.get_string_val()
''

Seperti yang Anda lihat dari contoh menggunakan kelas Foo , properti int_val dan string_val berfungsi baik untuk membaca dan menulis.

Tambahkan baris dokumentasi


Kami akan terus meningkatkan kelas Foo kami . Contoh berikut, yang terletak di folder pyfoo_cpp_03, memperlihatkan cara menambahkan garis dokumentasi (docstring) ke berbagai elemen kelas. Contoh ini didasarkan pada yang sebelumnya, dan perubahan utama di dalamnya menyangkut file pyfoocpp.sip . Berikut isinya:

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

class Foo {
%Docstring
Class example from C++ library
%End

    %TypeHeaderCode
    #include <foo.h>
    %End

    public:
        Foo(int, const char*);

        void set_int_val(int);
        %Docstring(format="deindented", signature="prepended")
            Set integer value
        %End

        int get_int_val();
        %Docstring(format="deindented", signature="prepended")
            Return integer value
        %End

        %Property(name=int_val, get=get_int_val, set=set_int_val)
        {
            %Docstring "deindented"
                The property for integer value
            %End
        };

        void set_string_val(const char*);
        %Docstring(format="deindented", signature="appended")
            Set string value
        %End

        char* get_string_val();
        %Docstring(format="deindented", signature="appended")
            Return string value
        %End

        %Property(name=string_val, get=get_string_val, set=set_string_val)
        {
            %Docstring "deindented"
                The property for string value
            %End
        };
};

Seperti yang sudah Anda pahami, untuk menambahkan baris dokumentasi ke elemen kelas apa pun, Anda perlu menggunakan arahan % Docstring . Contoh ini menunjukkan beberapa cara untuk menggunakan arahan ini. Untuk pemahaman yang lebih baik dari contoh ini, mari kita segera mengkompilasi paket pyfoocpp menggunakan perintah sip-wheel , instal, dan kami akan secara berurutan mencari tahu parameter mana dari direktif ini yang mempengaruhi apa, mengingat baris dokumentasi yang dihasilkan dalam mode perintah Python. Izinkan saya mengingatkan Anda bahwa baris dokumentasi disimpan sebagai anggota dari objek __doc__ tempat baris ini berada.

Dokumentasi baris pertama adalah untuk kelas Foo .. Seperti yang Anda lihat, semua baris dokumentasi terletak di antara arahan % Docstring dan % End . Baris 5-7 dari contoh ini tidak menggunakan parameter tambahan dari direktif % Docstring , sehingga baris dokumentasi akan ditulis ke kelas Foo apa adanya. Itulah sebabnya tidak ada lekukan di baris 5-7, jika tidak lekukan di depan baris dokumentasi juga akan jatuh ke Foo .__ doc__. Kami akan memastikan bahwa kelas Foo benar-benar berisi baris dokumentasi yang kami perkenalkan:

>>> from foocpp import Foo
>>> Foo.__doc__
'Class example from C++ library'

Arahan % Docstring berikut , terletak pada baris 17-19, menggunakan dua parameter sekaligus. Parameter format dapat mengambil salah satu dari dua nilai: "raw" atau "deindented". Dalam kasus pertama, baris dokumentasi disimpan saat ditulis, dan dalam kasus kedua, karakter spasi awal (tetapi bukan tab) dihapus. Nilai default untuk kasus ini jika parameter format tidak ditentukan dapat diatur menggunakan direktif % DefaultDocstringFormat (kami akan mempertimbangkannya sedikit kemudian), dan jika tidak ditentukan, maka diasumsikan bahwa format = "raw" .

Selain baris dokumentasi yang ditentukan, SIP menambahkan deskripsi tanda tangannya (jenis variabel apa yang diharapkan pada input dan apa jenis fungsi yang dikembalikan) ke baris dokumentasi fungsi. Parameter tanda tangan menunjukkan di mana harus meletakkan tanda tangan tersebut: sebelum baris dokumentasi yang ditentukan ( signature = "prepended" ), setelahnya ( signature = "ditambahkan" ) atau tidak menambahkan tanda tangan ( signature = "dibuang" ).

Contoh kami menetapkan parameter signature = "prepended" untuk fungsi get_int_val dan set_int_val , serta signature = "ditambahkan" untuk fungsi get_string_val dan set_string_val. Parameter format = "deindented" juga ditambahkan untuk menghapus spasi di awal baris dokumentasi. Mari kita periksa bagaimana parameter ini bekerja di Python:

>>> Foo.get_int_val.__doc__
'get_int_val(self) -> int\nReturn integer value'

>>> Foo.set_int_val.__doc__
'set_int_val(self, int)\nSet integer value'

>>> Foo.get_string_val.__doc__
'Return string value\nget_string_val(self) -> str'

>>> Foo.set_string_val.__doc__
'Set string value\nset_string_val(self, str)'

Seperti yang Anda lihat, menggunakan parameter tanda tangan dari direktif % Docstring , Anda dapat mengubah posisi deskripsi tanda tangan fungsi di baris dokumentasi.

Sekarang pertimbangkan untuk menambahkan baris dokumentasi ke properti. Perhatikan bahwa dalam kasus ini, % Docstring ... % Arahan akhir dilampirkan dalam kurung setelah direktif% Properti. Format rekaman ini dijelaskan dalam dokumentasi untuk arahan % Properti .

Perhatikan juga bagaimana kita menentukan parameter direktif % Docstring . Format penulisan arahan seperti itu dimungkinkan jika kita hanya menetapkan parameter pertama dari arahan (dalam hal ini, parameter format) Jadi, dalam contoh ini, tiga metode menggunakan arahan digunakan sekaligus.

Pastikan baris dokumentasi untuk properti diatur:

>>> Foo.int_val.__doc__
'The property for integer value'

>>> Foo.string_val.__doc__
'The property for string value'

>>> help(Foo)
Help on class Foo in module foocpp:

class Foo(sip.wrapper)
 |  Class example from C++ library
 |  
 |  Method resolution order:
 |      Foo
 |      sip.wrapper
 |      sip.simplewrapper
 |      builtins.object
 |  
 |  Methods defined here:
 |  
 |  get_int_val(...)
 |      get_int_val(self) -> int
 |      Return integer value
 |  
 |  get_string_val(...)
 |      Return string value
 |      get_string_val(self) -> str
 |  
 |  set_int_val(...)
 |      set_int_val(self, int)
 |      Set integer value
 |  
 |  set_string_val(...)
 |      Set string value
 |      set_string_val(self, str)
...


Mari sederhanakan contoh ini dengan menetapkan nilai default untuk format dan parameter tanda tangan menggunakan % DefaultDocstringFormat dan % arahan DefaultDocstringSignature . Penggunaan arahan ini ditunjukkan dalam contoh dari folder pyfoo_cpp_04 . File pyfoocpp.sip dalam contoh ini berisi kode berikut:

%Module(name=foocpp, language="C++")
%DefaultEncoding "UTF-8"
%DefaultDocstringFormat "deindented"
%DefaultDocstringSignature "prepended"

class Foo {
    %Docstring
    Class example from C++ library
    %End

    %TypeHeaderCode
    #include <foo.h>
    %End

    public:
        Foo(int, const char*);

        void set_int_val(int);
        %Docstring
            Set integer value
        %End

        int get_int_val();
        %Docstring
            Return integer value
        %End

        %Property(name=int_val, get=get_int_val, set=set_int_val)
        {
            %Docstring
                The property for integer value
            %End
        };

        void set_string_val(const char*);
        %Docstring
            Set string value
        %End

        char* get_string_val();
        %Docstring
            Return string value
        %End

        %Property(name=string_val, get=get_string_val, set=set_string_val)
        {
            %Docstring
                The property for string value
            %End
        };
};

Di awal file, baris % DefaultDocstringFormat "deindented" dan % DefaultDocstringSignature "prepended" ditambahkan , dan kemudian semua parameter dari arahan % Docstring dihapus.

Setelah mengumpulkan dan menginstal contoh ini, kita bisa melihat seperti apa gambaran kelas Foo sekarang , yang ditampilkan oleh perintah bantuan (Foo) :

>>> from foocpp import Foo
>>> help(Foo)

class Foo(sip.wrapper)
 |  Class example from C++ library
 |  
 |  Method resolution order:
 |      Foo
 |      sip.wrapper
 |      sip.simplewrapper
 |      builtins.object
 |  
 |  Methods defined here:
 |  
 |  get_int_val(...)
 |      get_int_val(self) -> int
 |      Return integer value
 |  
 |  get_string_val(...)
 |      get_string_val(self) -> str
 |      Return string value
 |  
 |  set_int_val(...)
 |      set_int_val(self, int)
 |      Set integer value
 |  
 |  set_string_val(...)
 |      set_string_val(self, str)
 |      Set string value
...

Semuanya terlihat cukup rapi dan berjenis sama.

Ganti nama kelas dan metode


Seperti yang sudah kami katakan, antarmuka yang disediakan oleh binding Python tidak harus cocok dengan antarmuka yang disediakan pustaka C / C ++. Kami menambahkan properti ke kelas di atas, dan sekarang kami akan melihat satu teknik lagi yang dapat berguna jika konflik nama atau fungsi kelas muncul, misalnya, jika nama fungsi cocok dengan beberapa kata kunci Python. Untuk melakukan ini, Anda bisa mengganti nama kelas, fungsi, pengecualian, dan entitas lainnya.

Untuk mengganti nama entitas, penjelasan PyName digunakan , nilai yang perlu ditetapkan nama entitas baru. Pekerjaan dengan anotasi PyName ditunjukkan dalam contoh dari folder pyfoo_cpp_05 . Contoh ini didasarkan pada contoh sebelumnya.pyfoo_cpp_04 dan berbeda dari itu oleh file pyfoocpp.sip , yang isinya sekarang terlihat seperti ini:

%Module(name=foocpp, language="C++")
%DefaultEncoding "UTF-8"
%DefaultDocstringFormat "deindented"
%DefaultDocstringSignature "prepended"

class Foo /PyName=Bar/ {
    %Docstring
    Class example from C++ library
    %End

    %TypeHeaderCode
    #include <foo.h>
    %End

    public:
        Foo(int, const char*);

        void set_int_val(int) /PyName=set_integer_value/;
        %Docstring
            Set integer value
        %End

        int get_int_val() /PyName=get_integer_value/;
        %Docstring
            Return integer value
        %End

        %Property(name=int_val, get=get_integer_value, set=set_integer_value)
        {
            %Docstring
                The property for integer value
            %End
        };

        void set_string_val(const char*) /PyName=set_string_value/;
        %Docstring
            Set string value
        %End

        char* get_string_val() /PyName=get_string_value/;
        %Docstring
            Return string value
        %End

        %Property(name=string_val, get=get_string_value, set=set_string_value)
        {
            %Docstring
                The property for string value
            %End
        };
};

Dalam contoh ini, kami mengganti nama kelas Foo ke kelas Bar , dan juga menetapkan nama lain untuk semua metode menggunakan anotasi PyName . Saya pikir semuanya di sini cukup sederhana dan jelas, satu-satunya hal yang patut diperhatikan adalah pembuatan properti. Dalam direktif % Properti , parameter get and set harus menentukan nama metode, karena mereka akan dipanggil dalam kelas Python, dan bukan nama yang semula dipanggil ke kode C ++.

Kompilasi contoh, instal, dan lihat bagaimana tampilan kelas ini dengan Python:

>>> from foocpp import Bar
>>> help(Bar)

Help on class Bar in module foocpp:

class Bar(sip.wrapper)
 |  Class example from C++ library
 |  
 |  Method resolution order:
 |      Bar
 |      sip.wrapper
 |      sip.simplewrapper
 |      builtins.object
 |  
 |  Methods defined here:
 |  
 |  get_integer_value(...)
 |      get_integer_value(self) -> int
 |      Return integer value
 |  
 |  get_string_value(...)
 |      get_string_value(self) -> str
 |      Return string value
 |  
 |  set_integer_value(...)
 |      set_integer_value(self, int)
 |      Set integer value
 |  
 |  set_string_value(...)
 |      set_string_value(self, str)
 |      Set string value
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)
 |  
 |  int_val
 |      The property for integer value
 |  
 |  string_val
 |      The property for string value
 |  
 |  ----------------------------------------------------------------------
...

Berhasil! Kami berhasil mengubah nama kelas itu sendiri dan metodenya.

Terkadang perpustakaan menggunakan perjanjian bahwa nama semua kelas dimulai dengan awalan, misalnya, dengan huruf "Q" di Qt atau "wx" di wxWidgets. Jika dalam penjilidan Python Anda ingin mengubah nama semua kelas, menghilangkan awalan seperti itu, maka agar tidak mengatur anotasi PyName untuk setiap kelas, Anda dapat menggunakan arahan % AutoPyName . Kami tidak akan mempertimbangkan arahan ini dalam artikel ini, kami hanya akan mengatakan bahwa arahan % AutoPyName harus berada di dalam direktif % Module dan membatasi diri pada contoh dari dokumentasi:

%Module PyQt5.QtCore
{
    %AutoPyName(remove_leading="Q")
}

Tambahkan konversi jenis


Contoh menggunakan kelas std :: wstring


Sejauh ini, kami telah melihat fungsi dan kelas yang telah bekerja dengan tipe sederhana seperti int dan char * . Untuk jenis ini, SIP secara otomatis membuat konverter ke dan dari kelas Python. Dalam contoh berikut, yang terletak di folder pyfoo_cpp_06 , kami akan mempertimbangkan kasus ketika metode kelas menerima dan mengembalikan objek yang lebih kompleks, misalnya, string dari STL. Untuk menyederhanakan contoh dan tidak menyulitkan konversi byte ke Unicode dan sebaliknya, kelas string std :: wstring akan digunakan dalam contoh ini . Ide dari contoh ini adalah untuk menunjukkan bagaimana Anda dapat secara manual mengatur aturan untuk mengkonversi kelas C ++ ke dan dari kelas Python.

Untuk contoh ini, kita akan mengubah kelas Foo dari perpustakaan foo. Sekarang definisi kelas akan terlihat seperti ini (file foo.h ):

#ifndef FOO_LIB
#define FOO_LIB

#include <string>

using std::wstring;

class Foo {
    private:
        int _int_val;
        wstring _string_val;
    public:
        Foo(int int_val, wstring string_val);

        void set_int_val(int val);
        int get_int_val();

        void set_string_val(wstring val);
        wstring get_string_val();
};

#endif

Implementasi kelas Foo dalam file foo.cpp :

#include <string>

#include "foo.h"

using std::wstring;

Foo::Foo(int int_val, wstring string_val):
    _int_val(int_val), _string_val(string_val) {}

void Foo::set_int_val(int val) {
    _int_val = val;
}

int Foo::get_int_val() {
    return _int_val;
}

void Foo::set_string_val(wstring val) {
    _string_val = val;
}

wstring Foo::get_string_val() {
    return _string_val;
}

Dan file main.cpp untuk memeriksa fungsionalitas perpustakaan:

#include <iostream>

#include "foo.h"

using std::cout;
using std::endl;

int main(int argc, char* argv[]) {
    auto foo = Foo(10, L"Hello");
    cout << L"int_val: " << foo.get_int_val() << endl;
    cout << L"string_val: " << foo.get_string_val().c_str() << endl;

    foo.set_int_val(0);
    foo.set_string_val(L"Hello world!");

    cout << L"int_val: " << foo.get_int_val() << endl;
    cout << L"string_val: " << foo.get_string_val().c_str() << endl;
}

File foo.h , foo.cpp dan main.cpp , seperti sebelumnya, terletak di folder foo . Proses makefile dan library build tidak berubah. Juga tidak ada perubahan signifikan pada file pyproject.toml dan project.py .

Tetapi file pyfoocpp.sip menjadi lebih rumit:

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

class Foo {
    %TypeHeaderCode
    #include <foo.h>
    %End

    public:
        Foo(int, std::wstring);

        void set_int_val(int);
        int get_int_val();
        %Property(name=int_val, get=get_int_val, set=set_int_val)

        void set_string_val(std::wstring);
        std::wstring get_string_val();
        %Property(name=string_val, get=get_string_val, set=set_string_val)
};

%MappedType std::wstring
{
%TypeHeaderCode
#include <string>
%End

%ConvertFromTypeCode
    // Convert an std::wstring to a Python (Unicode) string
    PyObject* newstring;
    newstring = PyUnicode_FromWideChar(sipCpp->data(), -1);
    return newstring;
%End

%ConvertToTypeCode
    // Convert a Python (Unicode) string to an std::wstring
    if (sipIsErr == NULL) {
        return PyUnicode_Check(sipPy);
    }
    if (PyUnicode_Check(sipPy)) {
        *sipCppPtr = new std::wstring(PyUnicode_AS_UNICODE(sipPy));
        return 1;
    }
    return 0;
%End
};

Untuk tujuan ilustrasi , file pyfoocpp.sip tidak menambahkan baris dokumentasi. Jika kami hanya meninggalkan deklarasi kelas Foo di file pyfoocpp.sip tanpa arahan % MappedType berikutnya , maka kami akan mendapatkan kesalahan berikut selama proses pembuatan:

$ sip-wheel

These bindings will be built: pyfoocpp.
Generating the pyfoocpp bindings...
sip-wheel: std::wstring is undefined

Kita perlu menjelaskan secara eksplisit bagaimana objek bertipe std :: wstring akan dikonversi ke beberapa objek Python, dan juga menjelaskan transformasi terbalik. Untuk menggambarkan konversi, kita perlu bekerja pada tingkat yang cukup rendah dalam bahasa C dan menggunakan API Python / C . Karena Python / C API adalah topik besar, bahkan layak untuk artikel terpisah, tetapi sebuah buku, di bagian ini kami hanya akan mempertimbangkan fungsi-fungsi yang digunakan dalam contoh, tanpa terlalu banyak detail.

Untuk mendeklarasikan konversi dari objek C ++ ke Python dan sebaliknya, direktif % MappedType dimaksudkan , di dalamnya mungkin ada tiga arahan lain: % TypeHeaderCode , % ConvertToTypeCode dan % ConvertFromTypeCode. Setelah ekspresi % MappedType , Anda perlu menentukan jenis konverter yang akan dibuat. Dalam kasus kami, arahan dimulai dengan ekspresi % MappedType std :: wstring . Kami telah memenuhi

arahan % TypeHeaderCode di bagian Membuat pengikatan untuk perpustakaan di C ++ . Biarkan saya mengingatkan Anda bahwa arahan ini dimaksudkan untuk menyatakan jenis yang digunakan atau untuk menyertakan file header di mana mereka dinyatakan. Dalam contoh ini , string file header , di mana kelas std :: string dideklarasikan, terhubung di dalam direktif % TypeHeaderCode . Sekarang kita perlu menggambarkan transformasi



% ConvertFromTypeCode. Mengubah objek C ++ menjadi Python


Kita mulai dengan mengonversi objek std :: wstring ke str kelas Python. Konversi dalam contoh ini adalah sebagai berikut:

%ConvertFromTypeCode
    // Convert an std::wstring to a Python (Unicode) string
    PyObject* newstring;
    newstring = PyUnicode_FromWideChar(sipCpp->data(), -1);
    return newstring;
%End

Di dalam direktif ini, kita memiliki variabel sipCpp - pointer ke objek dari kode C ++, dimana kita perlu membuat objek Python dan mengembalikan objek yang dibuat dari direktif menggunakan pernyataan kembali . Dalam hal ini, variabel sipCpp adalah tipe std :: wstring * . Untuk membuat kelas str , gunakan fungsi PyUnicode_FromWideChar dari API Python / C. Fungsi ini menerima array (pointer) dari tipe const wchar_t * w sebagai parameter pertama , dan ukuran array ini sebagai parameter kedua. Jika Anda melewatkan nilai -1 sebagai parameter kedua, maka fungsi PyUnicode_FromWideChar itu sendiri akan menghitung panjang menggunakan fungsi tersebutwcslen .

Untuk mendapatkan larik wchar_t * , gunakan metode data dari kelas std :: wstring .

Fungsi PyUnicode_FromWideChar mengembalikan pointer ke PyObject atau NULL jika terjadi kesalahan. PyObject adalah objek Python, dalam hal ini akan menjadi kelas str . Di API Python / C, bekerja dengan objek biasanya terjadi melalui pointer PyObject * , jadi dalam kasus ini, kami mengembalikan pointer PyObject * dari direktif % ConvertFromTypeCode .

% ConvertToTypeCode. Konversi objek Python ke C ++


Konversi terbalik dari objek Python (dasarnya dari PyObject * ) ke kelas std :: wstring dijelaskan dalam direktif % ConvertToTypeCode . Dalam contoh pyfoo_cpp_06, konversi adalah sebagai berikut:

%ConvertToTypeCode
    // Convert a Python (Unicode) string to an std::wstring
    if (sipIsErr == NULL) {
        return PyUnicode_Check(sipPy);
    }
    if (PyUnicode_Check(sipPy)) {
        *sipCppPtr = new std::wstring(PyUnicode_AS_UNICODE(sipPy));
        return 1;
    }
    return 0;
%End

Kode direktif % ConvertToTypeCode terlihat lebih rumit, karena selama proses konversi ia dipanggil beberapa kali untuk tujuan yang berbeda. Di dalam direktif % ConvertToTypeCode , SIP membuat beberapa variabel yang bisa (atau harus) kita gunakan.

Salah satu variabel ini, PyObject * sipPy, adalah objek Python yang Anda perlukan untuk membuat turunan dari kelas std :: wstring dalam kasus ini . Hasilnya perlu ditulis ke variabel lain - sipCppPtr adalah pointer ganda ke objek yang dibuat, yaitu dalam kasus kami, variabel ini akan bertipe std :: wstring ** . % ConvertToTypeCode lain

dibuat di dalam direktifvariabelnya adalah int * sipIsErr . Jika nilai variabel ini NULL , maka direktif % ConvertToTypeCode dipanggil hanya untuk memeriksa apakah konversi tipe dimungkinkan. Dalam hal ini, kami tidak berkewajiban untuk melakukan transformasi, tetapi hanya perlu memeriksa apakah mungkin secara prinsip. Jika memungkinkan, maka arahan harus mengembalikan nilai bukan nol, jika tidak, jika konversi tidak memungkinkan, mereka harus mengembalikan 0. Jika penunjuk ini bukan NULL , maka Anda perlu melakukan konversi, dan jika terjadi kesalahan selama konversi, kode kesalahan integer dapat disimpan ke dalam variabel ini (mengingat bahwa variabel ini adalah pointer ke int * ).

Dalam contoh ini, untuk memverifikasi bahwa sipPy adalah string unicode (kelas str ), makro PyUnicode_Check digunakan , yang mengambil argumen tipe PyObject * jika argumen yang dikirimkan adalah string unicode atau kelas yang diturunkan darinya.

Konversi ke objek C ++ dilakukan menggunakan string * sipCppPtr = new std :: wstring (PyUnicode_AS_UNICODE (sipPy)); . Ini memanggil PyUnicode_AS_UNICODE makro dari Python / C API, yang mengembalikan array bertipe Py_UNICODE * , yang setara dengan wchar_t * . Array ini diteruskan ke konstruktor dari std :: wstring class. Seperti disebutkan di atas, hasilnya disimpan dalam variabel sipCppPtr .

Saat ini, direktif PyUnicode_AS_UNICODE sudah usang dan disarankan untuk menggunakan makro lain, tetapi makro ini digunakan untuk menyederhanakan contoh.

Jika konversi berhasil, arahan % ConvertToTypeCode harus mengembalikan nilai bukan-nol (dalam hal ini 1), dan jika terjadi kesalahan ia harus mengembalikan 0.

Memeriksa


Kami menggambarkan konversi tipe std :: wstring ke str dan sebaliknya, sekarang kami dapat memastikan bahwa paket berhasil dibuat dan mengikat berfungsi sebagaimana mestinya. Untuk membangun, panggil sip-wheel , lalu instal paket menggunakan pip dan periksa operabilitas dalam mode perintah Python:

>>> from foocpp import Foo
>>> x = Foo(10, 'Hello')

>>> x.string_val
'Hello'

>>> x.string_val = ''
>>> x.string_val
''

>>> x.get_string_val()
''

Seperti yang dapat Anda lihat, semuanya berfungsi, tidak ada masalah dengan bahasa Rusia, mis. Konversi string Unicode dilakukan dengan benar.

Kesimpulan


Dalam artikel ini, kami membahas dasar-dasar menggunakan SIP untuk membuat binding Python untuk perpustakaan yang ditulis dalam C dan C ++. Pertama (di bagian pertama ) kami membuat pustaka sederhana di C dan menemukan file yang perlu dibuat untuk bekerja dengan SIP. File pyproject.toml berisi informasi tentang paket (nama, nomor versi, lisensi, dan jalur ke file header dan objek). Menggunakan file project.py , Anda bisa memengaruhi proses pembuatan paket Python, misalnya, mulai membangun pustaka C / C ++ atau mengizinkan pengguna untuk menentukan lokasi header dan file objek pustaka.

Dalam file * .sipmenjelaskan antarmuka modul Python yang mencantumkan fungsi dan kelas yang akan dimuat dalam modul. Arahan dan anotasi digunakan untuk menggambarkan antarmuka dalam file * .sip . Antarmuka kelas Python tidak harus cocok dengan antarmuka kelas C ++. Misalnya, Anda bisa menambahkan properti ke kelas menggunakan % direktif Properti , ganti nama entitas menggunakan / PyName / anotasi , dan tambahkan baris dokumentasi menggunakan arahan % Docstring .

Jenis dasar seperti int , char , char *dll. SIP secara otomatis mengkonversi ke kelas Python yang serupa, tetapi jika Anda perlu melakukan konversi yang lebih kompleks, Anda perlu memprogramnya sendiri di dalam direktif % MappedType menggunakan Python / C API. Konversi dari kelas Python ke C ++ harus dilakukan dalam arahan bersarang % ConvertToTypeCode . Konversi dari tipe C ++ ke kelas Python harus dilakukan dalam direktif bersarang % ConvertFromTypeCode .

Beberapa arahan seperti % DefaultEncoding , % DefaultDocstringFormat dan % DefaultDocstringSignature adalah penolong dan memungkinkan Anda untuk menetapkan nilai default untuk kasus-kasus ketika beberapa parameter penjelasan tidak diatur secara eksplisit.

Dalam artikel ini kami hanya memeriksa arahan dan penjelasan dasar dan paling sederhana, tetapi banyak dari mereka diabaikan. Misalnya, ada arahan untuk mengelola GIL, untuk membuat pengecualian Python baru, untuk mengelola memori dan pengumpul sampah secara manual, untuk kelas tweaker untuk sistem operasi yang berbeda, dan banyak lainnya yang dapat berguna ketika membuat ikatan perpustakaan C / C ++ yang kompleks. Kami juga menghindari masalah membangun paket untuk sistem operasi yang berbeda, membatasi diri untuk membangun di Linux menggunakan kompiler gcc / g ++.

Referensi



All Articles