[C] Varargs e passando eles adiante

Saindo do mundo C# e voltando para o “metal” C, uma coisa que pode parecer magia para quem nunca usou são as funções variádicas – funções com um número variável de argumentos. Quem nunca se perguntou como uma função como printf é implementada? Em Python (:heart:) nós temos os velhos conhecidos *args e **kwargs, que permitem coletar argumentos extras em uma tupla (posicionais) e um dicionários (argumentos nomeados), respectivamente. Mas em C, o processo é um pouco mais complicado, envolvendo um tipo (va_list) e três macros básicas (va_start, va_arg e va_end), todos disponíveis no header <stdarg.h>.

Exemplo básico:

Abaixo segue um pequeno exemplo de uma função – sum_all – que recebe um inteiro com um contador e uma lista variável de argumentos – que esperamos serem ints –  e retorna a soma desses argumentos variáveis.

#include <stdarg.h>
#include <stdio.h>

int sum_all(int count, ...) {
    va_list args;
    va_start(args, count);
    int acc = 0;

    for (int i=0; i < count; i++) {
        int value = va_arg(args, int);
        acc += value;
    }

    va_end(args);
    return acc;
}

int main() {
    printf("%d\n", sum_all(3, 1, 5, 1000));
}

Uma função variádica é declarada usando “” no final da lista de argumentos – a qual deve conter obrigatoriamente pelo menos um argumento com nome. Dentro do corpo da função por sua vez, você declara uma variável do tipo va_list e passa essa variável para a macro va_start junto com o nome da última variável antes da lista anônima. Esse segundo argumento é importante para o código saber de onde ele deve começar a buscar os argumentos.

Uma vez que temos a va_list inicializada, podemos então passar a pegar os argumentos propriamente ditos. Para isso utilizamos a macro va_arg, que recebe como argumento a va_list inicializada e o tipo esperado do argumento. Essa segunda informação é extremamente importante para o compilador saber qual o tamanho do argumento que ele deve buscar na lista. Ao final, va_end é chamada para limpar a va_list utilizada.

CUIDADO!

Duas observações: O compilador não faz *nenhuma* checagem de tipo entre o tipo passado numa chamada da função e o tipo que va_arg tenta extrair. Nada impede por exemplo de você chamar va_arg(args, double) dentro do loop acima e receber lixo. E diferente de Python onde você recebe uma tratável exceção, em C isso pode significar o programa simplesmente dar um belo segfault na cara do usuário.

A outra observação é quanto ao problema de se descobrir quando chegamos ao final da lista de argumentos. va_arg não dá informação nenhuma a respeito, já que ele apenas extrai da pilha – onde os argumentos são guardados – um valor do tamalho do tipo fornecido. Os dois modelos mais usados para resolver isso são usar um contador/string de formato e usar sentinelas. No primeiro caso, que é como o printf e nossa sum_all acima fazem, à medida que você vai parseando a string/incrementando o contador você sabe quando deve parar ou não. Já com o uso de sentinelas, ao parsear o argumento você determina valores (ex: NULL) que ao serem lidos indicarão que chegamos ao fim da lista.

Passando adiante

Agora digamos que você tenha uma bela função variádica mas você quer “decorar” ela com outra função sua – para fins de debug/log, por exemplo. Como podemos passar esses argumentos anônimos adiante? Passar a va_list diretamente não passaria todos os argumentos diretamente tal como seria uma chamada func(*args) em Python. Na verdade, passar a va_list é equivalente a chamar func(args) – a função recebe um único argumento com a va_list.

E é aproveitando essa última informação que uma boa prática com varargs é fornecer uma variante da função alvo, só que recebendo uma va_list. Por exemplo, temos o par printf e vprintf. A primeira é variádica e na prática é implementada em termos da segunda, que recebe a va_list. Traduzindo para nosso exemplo acima:

int vsum_all(int count, va_list args) {
    int acc = 0;
    for (int i=0; i < count; i++) {
        int value = va_arg(args, int);
        acc += value;
    }
    return acc;
}

int sum_all(int count, ...) {
    va_list args;
    va_start(args, count);

    int acc = vsum_all(count, args);

    va_end(args);
    return acc;
}

E caso você necessite utilizar uma va_list com os argumentos antes de passar eles adiantes, você pode utilizar a va_copy(mylist) para incializar uma nova va_list com uma cópia da va_list original.

o/

Anúncios

C#, COM, OLE e threads

Seguindo as aventuras no mundo dos bindings C# para a EFL, um problema que enfrentei semana passada durante o port dos bindings para o Windows envolvia a famigerada API Win32. Tudo estava correndo bem até tentar mostrar algo na tela, quando os módulos de UI simplesmente se recusavam a carregar, ao contrário de quando tentávamos rodar apenas o código nativo, sem o binding. O culpado: OleInitialize.

A EFL no Windows utiliza por baixo dos panos a Win32, API tradicional para apps nativas do sistema operacional. Entre os componentes utilizados, está o velho OLE (Object Linking and Embedding), responsável por compartilhar itens entre aplicações, como por exemplo os serviços de drag and drop e clipboard, desde o começo dos anos 90. Ao longo do tempo, o OLE acabou gerando o COM (Component Object Model), que serve comunicação interprocessos para outras tecnologias do Windows – numa analogia meio “grosseira”, seria uma espécie de “DBus do Windows”, porém pelo menos 10 anos mais velho.

Para lidar com threading, o COM agrupa os objetos em apartments, que podem ser single thread – todas as chamadas a um objeto saem da mesma thread – e multi thread – cada objeto cuida de sua própria sincronização e chamadas podem vir de múltiplas threads. Entre outras coisas, o Oleinitialize cuida de inicializar o COM caso este já não esteja inicializado, e obrigatoriamente no modo single-thread. Caso a função retorne RPC_E_CHANGED_MODE, significa que o COM já foi inicializado anteriormente em modo multi-thread. E era exatamente isso que estava acontecendo.

Investigando mais um pouco, o principal suspeito era o próprio runtime do .NET. Verificando o número de threads com System.Diagnostics.Process.GetCurrentProcess().Threads.Count, um simples “hello world” indicava 6 threads em execução. E ao inspecionar o código do Mono, realmente o COM era inicializado em modo multithread. E a resposta é dada na documentação do atributo STAThreadAttribute (livre tradução):

A partir da versão 2.0 do framework .NET, o modelo de threading padrão da interoperabilidade COM depende da linguagem em que se está desenvolvendo a aplicação:

  • C++/C#: Multi-thread
  • Visual Basic: Single thread.

Prevendo esse tipo de problema, .NET oferece para C# o já mencionado atributo STAThreadAttribute, que obrigatoriamente deve decorar o Main() da aplicação para indicar que a interoperabilidade COM deve excepcionalmente ser inicializada como single thread. Para aplicações C++, a flag /CLRTHREADATTRIBUTE:STA deve ser fornecida para o linker.

o/