Lendo o código de funções em arquivos e executando…
Continuando o post anterior…
Uma vez que temos o código de uma função gravado em um arquivo, para executá-la devemos tomar algumas precauções:
1) Garantir que o cabeçalho da função é o mesmo – isso vale para tipos de parâmetros e tipos de valor de retorno, especialmente quando se usa um tipo definido pelo usuário – nesse exemplo, a struct Matrizes.
2) Lembrar que a função sendo lida do arquivo não foi linkeditada com este programa, logo ela não pode usar nada que não seja local. Se houver a necessidade de se chamar uma função externa (por exemplo, chamar a função sin), devemos passar um ponteiro para função.
3) Constantes podem ser utilizadas desde que tenham o mesmo valor, obviamente.
Segue o programa para ler o código de uma função em um arquivo e executá-la. A função que faz a leitura é leFuncao. Ela recebe um nome de arquivo e uma referência para CodigoFuncao, que nada mais é do que um ponteiro para void. Logo, ela recebe uma referência para um ponteiro, já que ela irá alocar uma área para receber a função a ser lida. Essa área deve ser alocada, no Windows, via uma chamada à função da API VirtualAlloc. No Linux não testei, mas pelo que sei seria utilizando a função mmap. O Windows, assim como o Linux e diversos outros SO’s, dividem a memória em áreas de código e de dados, de modo que não é permitido escrever na área de código e nem executar algo a partir da área de dados. Para contornar isso, é necessário informar ao SO que queremos uma área de memória com permissão de escrita e de execução, daí a necessidade de uma chamada a API passando o parâmetro PAGE_EXECUTE_READWRITE. Feito isso, basta obter o tamanho da função (no caso, o mesmo do arquivo), alocar o espaço com o tamanho desejado e ler a função para este espaço. No programa principal, o ponteiro para o código (que é void*) é convertido para ponteiro para função, com os tipos corretos (responsabilidade do programador…). O programa recebe o nome do arquivo a ser lido como primeiro parâmetro da linha de comando.
Código Fonte do programa que lê e executa uma função:
#include <cstdlib> #include <iostream> #include <windows.h> #include <winbase.h> using namespace std; typedef void* CodigoFuncao; const int N = 1000; struct Matrizes { double a[N][N], b[N][N], c[N][N]; }; typedef void (*PtrFuncao)( Matrizes& ); int tamanhoArquivo( FILE* fp ) { int tamanho; fseek( fp, 0L, SEEK_END ); tamanho = ftell( fp ); fseek( fp, 0L, SEEK_SET ); return tamanho; } void leFuncao( const char* nomeArquivo, CodigoFuncao& codigo ) { FILE *arquivo = fopen( nomeArquivo, "rb" ); int tamanhoFuncao = 0; if( arquivo == NULL ) { cout << "Erro ao abrir arquivo " << nomeArquivo << endl; exit( 0 ); } tamanhoFuncao = tamanhoArquivo( arquivo ); codigo = (CodigoFuncao) VirtualAlloc( NULL, tamanhoFuncao, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); fread( codigo, 1, tamanhoFuncao, arquivo ); fclose( arquivo ); cout << "Leu funcao do arquivo: " << nomeArquivo << "." << endl; cout << "Tamanho da funcao: " << tamanhoFuncao << " bytes." << endl << endl; } int main(int argc, char *argv[]) { Matrizes& m = *new Matrizes; PtrFuncao funcao = NULL; if( argc != 2 ) { cout << "Uso: lefuncao nomeArquivo" << endl; exit(0); } srand(1); for( int i = 0; i < N; i++ ) for( int j = 0; j < N; j++ ) { m.a[i][j] = rand()/(rand()+0.1); m.b[i][j] = rand()/(rand()+0.1); m.c[i][j] = 0; } leFuncao( argv[1], (CodigoFuncao&) funcao ); funcao( m ); delete &m; return 0; } |
Saída para a linha de comando “lefuncao funcao1.dat” :
Leu funcao do arquivo: funcao1.dat. Tamanho da funcao: 160 bytes. |
Saída para a linha de comando “lefuncao funcao2.dat” :
Leu funcao do arquivo: funcao2.dat. Tamanho da funcao: 208 bytes. |
Detalhes:
A primeira execução leva como era de se esperar 16,03 segundos. Já a segunda, que representa a versão otimizada o algoritmo de multiplicação de matrizes, 2,73 segundos. Note que esse programa está realmente executando a função de um outro programa…Não há nenhum código de multiplicação de matrizes no programa acima!
Na verdade é possível até salvar a função compilada pelo compilador Intel e executá-la através desse programa, que foi compilado no g++… Nesse caso, o tempo dá 1,19 segundos e a função fica com 408 bytes. Um pouco mais lento que na versão original pois o otimizador do compilador Intel não consegue realizar as mesmas otimizações quando a multiplicação de matrizes se torna uma função. Discutirei isso depois, o importante a notar é que pode-se misturar códigos de outros compiladores… desde que a convenção de chamadas de funções e passagem de parâmetros seja a mesma (stack frame), tudo funciona!
Por último, como mencionado no início do post, se quisermos salvar uma função que utiliza outras funções, devemos passar as outras funções como parâmetros (ponteiros). Qualquer tipo de função pode ser passada, mas métodos e objetos não vão funcionar pelo mesmo problema de linkedição – será necessário transformar uma chamada de um método em uma função que recebe o objeto como parâmetro, por exemplo. Por outro lado, funções (e métodos) garantidamente inline vão funcionar – mas usarão a expansão de código existente no arquivo fonte de onde a função foi compilada.
Chamando outras funções:
double formulaSecreta( double x, double (*f)( double ), int printf(const char*, ... ), const char* format ) {
double total = 0,
potencia = 1;
for( int i = 0; i < 5; i++ ) {
printf( format, i, potencia, total );
total += f(x)/potencia;
potencia *= x;
}
return total;
}
|
Note que esta função espera receber um double, um ponteiro para um função que recebe um double e retorna um double, a função printf (usei o mesmo nome de propósito) e a cadeia de formatação para a printf – embora cadeias de caracteres estáticas sejam constantes, elas não são armazenadas junto com o código da função, logo são como variáveis globais e portanto, têm de ser passadas também como parâmetros. Se o número de funções aumentar demais, é melhor passar uma struct com vários ponteiros para as funções…
É… C++… que linguagem é essa que permite uma coisa doida assim!!!
“A inteligência, Deus no-la deu… No-la deu. Que língua, a nossa!”
Ótima sequência de artigos!
Um uso para essa técnica seria encriptar funções-chave para depois usá-las, por exemplo, em registro de software. E falando em registro de software, é possível inclusive enviar a própria função como código de validação, encriptada pelo nome do usuário. Apenas devaneando, no espírito do tema =)
[]s
Obrigado!
Realmente esse é um uso para o qual eu não tinha devaneado… rs
[]s