Rust: Funções Rust que podem ser chamadas de C

Criado em 2 fev. 2012  ·  16Comentários  ·  Fonte: rust-lang/rust

Temos muitos cenários agora em que as pessoas que criam ligações desejam ser capazes de fornecer um retorno de chamada que uma função C pode chamar. A solução atual é escrever uma função C nativa que usa algumas APIs internas não especificadas para enviar uma mensagem de volta ao código Rust. Idealmente, não envolve escrever código C.

Aqui está uma solução mínima para criar funções em Rust que podem ser chamadas a partir do código C. A essência é: 1) temos ainda outro tipo de declaração de função, 2) esta função não pode ser chamada a partir do código Rust, 3) seu valor pode ser considerado um ponteiro opaco inseguro, 4) ele incorpora a magia de troca de pilha e se adapta do C ABI para o Rust ABI.

Declarações de funções C-to-Rust (crosta):

crust fn callback(a: *whatever) {
}

Obtendo um ponteiro não seguro para uma função C ABI:

let callbackptr: *u8 = callback;

Também poderíamos definir algum tipo especificamente para esse propósito.

Implementação do compilador:

É mais direto, mas trans fica feio. Em trad, precisaremos fazer basicamente o oposto do que fazemos para funções de mod nativas:

  • Gere uma função C ABI usando a assinatura declarada
  • Gere uma função shim que leva os argumentos C em uma estrutura
  • A função C coloca os argumentos em uma estrutura
  • A função C chama upcall_call_shim_on_rust_stack com a estrutura de argumentos e o endereço da função shim
  • Gere uma função Rust ABI usando a assinatura declarada
  • A função shim extrai os argumentos da estrutura e chama a função Rust

Implementação de tempo de execução:

O tempo de execução precisa mudar de algumas maneiras para que isso aconteça:

  • Um novo upcall para voltar para a pilha Rust
  • As tarefas precisam manter uma pilha de contextos Rust e contextos C
  • Precisa de uma estratégia para lidar com a falha após entrar novamente na pilha do Rust
  • Precisa de uma estratégia para lidar com a cedência após entrar novamente na pilha de Rust

Fracasso:

Não podemos simplesmente lançar uma exceção depois de entrar novamente na pilha Rust porque não há garantia de que o código nativo possa ser desfeito com exceções C ++. A linguagem Go aparentemente irá simplesmente pular todos os frames nativos neste cenário, vazando tudo ao longo do caminho. Em vez disso, iremos abortar - se o usuário quiser evitar uma falha catastrófica, ele deve usar o retorno de chamada do Rust para enviar uma mensagem e retornar imediatamente.

Produzindo:

Sem mudanças na maneira como lidamos com as pilhas C, não podemos permitir que as funções do Rust mudem de contexto para o agendador depois de entrar novamente na pilha do Rust a partir do código C. Vejo duas soluções:

1) O rendimento é diferente depois de entrar novamente na pilha do Rust e simplesmente bloquear. As tarefas que desejam fazer isso devem ter certeza de que têm seu próprio agendador (# 1721).
2) Em vez de executar o código nativo usando a pilha do agendador, as tarefas verificarão as pilhas C de um pool localizado em cada agendador. Cada vez que uma tarefa entrar novamente na pilha C, ela verificará se já tem uma e a reutilizará, caso contrário, solicitará uma nova ao escalonador. Isso permitiria que o código do Rust sempre rendesse normalmente, sem amarrar o agendador.

Prefiro a segunda opção.

Veja também # 1508

A-debuginfo A-runtime A-typesystem E-easy

Comentários muito úteis

Por favor, perdoe esta ressurreição, mas este problema está vinculado a uma peça do combinador y e alguns outros sites, e recentemente um novato perguntou sobre isso, então estou observando que, por este problema e alterações posteriores, chamando Rust de C é simples :

#[no_mangle]
pub extern fn hello_rust() -> *const u8 {
    "Hello, world!\0".as_ptr()
}
#include "stdio.h"
const char *hello_rust(void);
int main(void) {
    printf("%.32s\n", hello_rust());
}

Todos 16 comentários

Desculpe pular aqui, mas eu gostaria de enfatizar que chamar funções C deve ser rápido, como em _blazing_ fast. Se eu quiser escrever um jogo enferrujado usando uma biblioteca C como Allegro, SDL ou Opengl, isso é essencial. Caso contrário, o jogo ficará lento no código de renderização, onde há muitas chamadas C, o que é inaceitável. O compilador de linguagem Go padrão com cgo tem esses problemas.

Portanto, prefiro uma solução rápida, embora possa restringir o que a função do lado do Rust pode fazer.

Além disso, não seria uma ideia usar "fn nativo" em vez de "fn crosta" ou isso tem outro significado planejado?

@beoran você tem isso ao contrário. Estamos falando de C chamar Rust. Chamar funções C do Rust já é bem rápido (poderia ser um pouco mais rápido).

OK eu vejo. Existe alguma maneira de ajudar a acelerar a chamada de C da ferrugem?

@beoran como eu disse, é meio ortogonal a este problema ... mas provavelmente a melhor coisa que você poderia fazer é criar um benchmark mostrando como o desempenho é inadequado. :)

OK, farei isso quando chegar longe o suficiente no empacotamento do Allegro para comparar a sobrecarga de chamá-lo da ferrugem Rust com chamá-lo de C. Vou deixar esse problema de lado por enquanto e abrirei um novo assim que tiver o benchmark.

Poderíamos evitar uma das cópias dos argumentos fazendo com que a função C fosse escrita diretamente na pilha Rust, como as chamadas Rust-> C fazem atualmente?

Espero que a cópia final dos argumentos fora da estrutura shim e nos argumentos da função ferrugem seja eliminada por meio do embutimento. Não tenho certeza se é a isso que você está se referindo.

Acredito que nossas chamadas C atualmente copiam os argumentos em uma estrutura na pilha Rust e, em seguida, copiam essa estrutura na pilha C.

@pcwalton Rust-> C atualmente não grava diretamente na pilha C porque isso é específico para i386. Eu queria evitar ter que escrever um código específico para uma convenção de chamada em particular, mesmo ao preço de algum desempenho, para fazer o 64 bits funcionar. (Eu acho que tais otimizações podem fazer sentido agora, entretanto --- particularmente como # 1402 aponta que o LLVM realmente não lida com as convenções de chamada completamente _de qualquer forma_)

Depois de ler a função de troca de pilha, o arg struct não é copiado entre as pilhas, o ponteiro para a pilha anterior é apenas passado para a função executada na nova pilha, o que faz todo o sentido.

O arg struct não é copiado, mas a função shim carregará os valores dele e os reenviará para a nova pilha. O código antigo costumava escrever literalmente os valores dos argumentos diretamente na pilha de destino. Isso fazia sentido em i386, mas em x86_64 é muito mais complexo descobrir quais valores irão para a pilha, etc.

Depois de alguns testes, descobri que será muito difícil para as funções da crosta garantir que não falhem. O que acontece atualmente é que, quando uma tarefa de nível superior (como principal) falha, todas as tarefas são avisadas para falhar, então, assim que o callback tenta enviar uma mensagem (ou quando retorna após o envio de uma mensagem), pode acabar falhando e fazendo com que o tempo de execução seja interrompido de forma anormal.

Acho que podemos alterar rust_task para ignorar solicitações de eliminação, uma vez que as tarefas tenham entrado novamente na pilha de ferrugem. Para tarefas que implementam loops de eventos, eles podem espiar em alguma porta do monitor à procura de uma mensagem indicando que o tempo de execução está falhando e descobrir como encerrar normalmente.

Então, quando uma tarefa de nível superior falha, ela propaga erros para seus filhos? Acho que não entendi nosso modelo de propagação de erros, pensei que fosse das folhas para cima. Parece que o código executado em pilhas C deve ser capaz de ser executado em uma tarefa não supervisionada ou algo parecido.

Ele basicamente age como se 'main' fosse supervisionado pelo kernel, então se main falhar, tudo falha.

Estou chamando isso de feito. Há uma pequena limpeza e arquivei bugs separados para os problemas restantes.

Por favor, perdoe esta ressurreição, mas este problema está vinculado a uma peça do combinador y e alguns outros sites, e recentemente um novato perguntou sobre isso, então estou observando que, por este problema e alterações posteriores, chamando Rust de C é simples :

#[no_mangle]
pub extern fn hello_rust() -> *const u8 {
    "Hello, world!\0".as_ptr()
}
#include "stdio.h"
const char *hello_rust(void);
int main(void) {
    printf("%.32s\n", hello_rust());
}
Esta página foi útil?
0 / 5 - 0 avaliações