[2] Integrando Python com C/C++ ~ Passagem de parâmetros e retorno

[2] Integrando Python com C/C++ ~ Passagem de parâmetros e retorno

Na postagem anterior, introduzi o conceito de shared library e demonstrei uma forma simples de usar a integração com o Python. No caso, como passar duas variáveis inteiras e retornar a soma dela. Porém, isso é limitado. Podemos fazer muito mais do que apenas isso.

Aqui está a lista completa dos tipos do ctypes. Os tipos do ctypes é uma tradução dos tipos do Python para o do C, servindo de ponte. Essa ponte é bidirecional, assim podemos traduzir tanto do Python para o C como do C para o Python.

Apenas chamar uma função no C

#include <stdio.h>

void only_make_it() {
    printf("Macabeus me mandou apenas imprimir isso!\n");
}
import ctypes

c_lib = ctypes.CDLL('ccode/libmytest.so')
c_lib.only_make_it.restype = None
c_lib.only_make_it.argtypes = ()
c_lib.only_make_it() # Imprimirá "Macabeus me mandou apenas imprimir isso!\n"

Caso nenhum retorno seja dado, deve-se setar com None o retorno, como feito na linha 4. Como nenhum parâmetro é necessário, podemos setar com uma tupla vazia a lista de parâmetros, como feito na linha 5. Ninguém vai te condenar se não setá-la com uma tupla vazia, bastando apenas deixar de escrever essa linha, porém, eu pessoalmente tenho esse hábito, para deixar o código uniforme.

Passar parâmetros inteiros e retornar um inteiro

int adder(int a, int b) {
    return a + b;
}
c_lib.adder.restype = ctypes.c_int
c_lib.adder.argtypes = (ctypes.c_int, ctypes.c_int)
print(c_lib.adder(2, 3)) # Imprimirá 5

Código já explicado na postagem anterior.

Passar uma array do Python e modifica-la no C

void change_array(int* python_array, int length) {
    int i;
    for (i = 0; i < length; i++) {
        python_array[i] = python_array[i] * 2;
    }
}
c_lib.change_array.restype = None
c_lib.change_array.argtypes = (ctypes.POINTER(ctypes.c_int), ctypes.c_int)

x = [2, 3, 4]
param = (ctypes.c_int * len(x))(*x)

c_lib.change_array(param, len(x))

print([i for i in param]) # Imprimirá [4, 6, 8]

Você, assim como eu, levantou uma sobrancelha em (ctypes.c_int * len(x))(*x). Calma, irei esclarece-la.

Usando a função POINTER do ctypes, vai transformar o argumento num ponteiro. No caso, durante a chamada de change_array feita na linha 7, ao invés de copiar o valor do parâmetro param, será passada apenas o endereço no Python dele. Desse modo, podemos editar variáveis do Python no C.

Porém, não podemos passar uma array "pura" do Python. Precisamos adapta-la, assim criamos uma cópia dela usando os tipos do ctypes. Na linha 5 fizemos isso. Ao multiplicarmos qualquer tipo do ctypes, como o c_int, estamos criando uma array com tamanho usado na multiplicação. Usando *x estamos distribuindo o valor de x respectivamente para cada parte da array. Lembre-se que esse é o operador unpack do Python.

Passar uma string do Python para o C imprimir

void print_string(char *python_string) {
    printf("-> %s <-", python_string);
}
c_lib.print_string.restype = None
c_lib.print_string.argtypes = (ctypes.c_char_p,)

x = 'macabeus'.encode('ascii')

c_lib.print_string(x) # Imprimirá "-> macabeus <-"

Lembre-se: o Python 3 nativamente trabalha com Unicode, enquanto o C é ASCII, por isso devemos mudar a codificação antes de passarmos para o C imprimir.

Receber uma estrutura do C

struct thing {
    char letter;
    float value;
};

struct thing get_struct() {
    struct thing my_thing;
    my_thing.letter = 'm';
    my_thing.value = 10.91;

    return my_thing;
}
class Thing(ctypes.Structure):
    _fields_ = (('letter', ctypes.c_char),
                ('value', ctypes.c_float))

c_lib.get_struct.restype = Thing
c_lib.get_struct.argtypes = ()

temp = c_lib.get_struct()
print(temp.letter, temp.value) # Imprimirá "b'm' 10.90999984741211"

O Python precisa saber os elementos da estrutura, e, para isso, criamos uma classe que herda Structure. Nela, definidos no atributo da classe fields os elementos da estrutura. Precisa estar exatamente na mesma ordem do definido no C, caso contrário bagunçará o deslocamento das variáveis.

Nesse exemplo, batizamos com os mesmos nomes os atributos da estrutura tanto no C como no Python, porém, isso não é obrigatório - apesar de assim ficar bem mais organizado.

O Thing é como se fosse um novo tipo, e podemos usa-lo da mesma forma que usamos os demais tipos do ctypes. Desse modo, atribuímos como retorno na linha 5.

Passar uma array de estrutura ao C e retorna a array com as estruturas alteradas

void change_struct(struct thing python_things[]) {
    int i;
    for (i = 0; i < 3; i++) {
        python_things[i].letter = 97 + i;
        python_things[i].value = 0.5 * i;
    }
}
c_lib.change_struct.restype = None
c_lib.change_struct.argtypes = (ctypes.POINTER(Thing),)

param = (Thing * 10)(Thing())

c_lib.change_struct(param)
for i in param:
    if i.letter == b'x00':
        break
    print(i.letter, i.value)

Esse exemplo se assemelha levemente com o "Passar uma array do Python e modifica-la no C". No caso, a array criada no Python tem 10 elementos. No C, editamos os três primeiros. Se o C tentasse editar o 11º elemento, estaria acessando a área de memória de alguma outra coisa.

Repare que no exemplo da array numérica havíamos usado param = (ctypes.c_int * len(x))(*x) enquanto aqui foi param = (Thing * 10)(Thing()).

Aqui não havia sentido algum usar o operador unpack, pois não desejamos distribuir valores no decorrer da array criada. Apenas desejamos uma cópia do objeto de Thing.

O objeto de Thing não necessariamente precisa ter todos os atributos começando vazios, como no exemplo acima. Poderíamos já começar um objeto de Thing com alguns dados, e a array também, assim ficando:

class ThingStarted(Thing):
    def __init__(self):
        super().__init__(value=10)

param = (Thing * 10)(*[ThingStarted() for i in range(10)])

Como ThingStarted é uma classe herdade de Thing, não precisamos alterar o tipo de retorno de change_struct.

Usar uma variável no heap criada no C

char* get_string() {
    char* buff = (char*) malloc(sizeof(char) * 15);
    strcpy(buff, "Macabeus Lindo");

    printf("buff: %pn", buff);

    return buff;
}

void freeme(void* pointer) {
    printf("free: %pn", pointer);

    free(pointer);
}
c_lib.get_string.restype = ctypes.POINTER(ctypes.c_char)
c_lib.get_string.argtypes = ()

c_lib.freeme.restype = None
c_lib.freeme.argtypes = (ctypes.c_void_p,)

string_pointer = c_lib.get_string()
string_value = ctypes.cast(string_pointer, ctypes.c_char_p).value.decode('ascii')
print(string_value)

c_lib.freeme(string_pointer)

Tivemos que criar duas funções no C: uma para retornar uma variável no heap (no caso, uma string contendo uma excelente frase) e uma função para liberar um espaço no heap. Isso é necessário porque o Python nada sabe sobre o heap da shared library.

A função get_string apenas retornará o ponteiro, e então atribuímos seu retorno para a variável string_pointer. Então, agora precisamos do valor realmente útil para nós, para isso devemos castar, alterando o valor referenciado pelo ponteiro para a string. Para isso, usamos a função cast de ctypes. O seu primeiro parâmetro é o ponteiro e o segundo é para qual tipo irá. No caso, c_char_p traduz toda aquela sequência de chars do C em um objeto string do Python. Então pegamos o valor do cast e, finalmente, traduzimos do ASCII para Unicode.

Em seguida, nós liberamos a memória que ocupamos chamando a função freeme.

Fim

Agora que você já deve ter virado o lorde da integração de C/C++ no Python, você certamente deseja compartilhar o seu pacote para os mortais. Veja como fazer isso nessa terceira postagem da série.