Gerando tipos de personagem em tempo real (ou maluco com Rust)

Neste artigo, vamos tirar sarro da linguagem de programação Rust e, em particular, dos objetos de características.


Quando me familiarizei com Rust, um dos detalhes da implementação de objetos de tipo parecia interessante para mim. Nomeadamente, a tabela de funções virtuais não está nos próprios dados, mas no ponteiro "grosso" para ele. Cada ponteiro para um objeto de tipo) contém um ponteiro para os próprios dados, bem como um link para uma tabela virtual onde os endereços das funções que implementam esse objeto de tipo para uma determinada estrutura serão localizados (mas, como esse é um detalhe da implementação, o comportamento pode mudar.


Vamos começar com um exemplo simples demonstrando indicadores grossos. O código a seguir será exibido nas arquiteturas de 64 bits 8 e 16:


fn main () { let v: &String = &"hello".into(); let disp: &std::fmt::Display = v; println!("  : {}", std::mem::size_of_val(&v)); println!("   -: {}", std::mem::size_of_val(&disp)); } 

Por que isso é interessante? Quando eu estava envolvido no Java corporativo, uma das tarefas que surgia com bastante regularidade era a adaptação dos objetos existentes a determinadas interfaces. Ou seja, o objeto já existe, emitido como um link, mas deve ser adaptado à interface especificada. E você não pode alterar o objeto de entrada, é o que é.


Eu tive que fazer algo assim:


 Person adapt(Json value) { // ...- , , ,  "value"  //   Person return new PersonJsonAdapter(value); } 

Houve vários problemas com essa abordagem. Por exemplo, se o mesmo objeto "se adaptar" duas vezes, obteremos duas pessoas diferentes (do ponto de vista da comparação de links). E o fato de você precisar criar novos objetos toda vez é feio.


Quando vi objetos de texto no Rust, tive a ideia de que no Rust isso poderia ser feito de maneira muito mais elegante! Você também pode pegar e atribuir outra tabela virtual aos dados e obter um novo objeto de característica! E não aloque memória para cada instância. Ao mesmo tempo, toda a lógica do "empréstimo" permanece em vigor - nossa função de adaptação se parecerá com algo como fn adapt<'a>(value: &'a Json) -> &'a Person (ou seja, nós meio que emprestamos de dados de origem).


Ainda mais que isso, você pode "forçar" o mesmo tipo (por exemplo, String ) a implementar nosso objeto de tipo várias vezes, com comportamento diferente. Porque Mas você nunca sabe o que pode ser necessário na empresa ?!


Vamos tentar implementar isso.


Declaração do problema


Definimos a tarefa da seguinte maneira: crie a função de annotate , que "atribui" o seguinte objeto de tipo ao tipo String regular:


 trait Object { fn type_name(&self) -> &str; fn as_string(&self) -> &String; } 

E a função de annotate si:


 ///    - `Object`,   , ///   "" -- ,    `type_name`. fn annotate<'a>(input: &'a String, type_name: &str) -> &'a dyn Object { // ... } 

Vamos escrever um teste imediatamente. Primeiro, verifique se o tipo "atribuído" corresponde ao esperado. Em segundo lugar, garantiremos a obtenção da linha original e ela será a mesma (do ponto de vista dos ponteiros):


 #[test] fn test() { let input: String = "hello".into(); let annotated1 = annotate(&input, "Widget"); let annotated2 = annotate(&input, "Gadget"); // -   ,    assert_eq!("Widget", annotated1.type_name()); assert_eq!("Gadget", annotated2.type_name()); let unwrapped1 = annotated1.as_string(); let unwrapped2 = annotated2.as_string(); //       --   assert_eq!(unwrapped1 as *const String, &input as *const String); assert_eq!(unwrapped2 as *const String, &input as *const String); } 

Abordagem número 1: e depois de nós pelo menos uma inundação!


Primeiro, vamos tentar fazer uma implementação completamente ingênua. Basta agrupar nossos dados em um "invólucro", que conterá adicionalmente type_name :


 struct Wrapper<'a> { value: &'a String, type_name: String, } impl<'a> Object for Wrapper<'a> { fn type_name(&self) -> &str { &self.type_name } fn as_string(&self) -> &String { self.value } } 

Nada de especial ainda. Tudo é como em Java. Mas não temos um coletor de lixo, onde armazenaremos esse invólucro? Precisamos retornar o link, para que ele permaneça válido depois de chamar a função de annotate . Colocaremos algo assustador na Box para que o Wrapper destacado na pilha. E então retornaremos o link para ele. E para que o wrapper permaneça ativo depois de chamar a função de annotate , "vazaremos" esta caixa:


 fn annotate<'a>(input: &'a String, type_name: &str) -> &'a dyn Object { let b = Box::new(Wrapper { value: input, type_name: type_name.into(), }); Box::leak(b) } 

... e o teste passa!


Mas esta é uma decisão duvidosa. Além de alocarmos memória a cada "anotação", a memória vaza ( Box::leak retorna um link para os dados armazenados na pilha, mas ao mesmo tempo "esquece" a própria caixa, ou seja, não haverá liberação automática) )


Abordagem 2: Arena!


Para começar, vamos tentar salvar esses wrappers em algum lugar para que eles sejam liberados em algum momento. Mas, ao mesmo tempo, mantendo a assinatura annotate como ela é. Ou seja, retornar um link com contagem de referência (por exemplo, Rc<Wrapper> ) não funciona.


A opção mais simples é criar uma estrutura auxiliar, um "sistema de tipos", que será responsável por armazenar esses wrappers. E quando terminarmos, lançaremos essa estrutura e todos os invólucros com ela.


Algo assim. A biblioteca typed-arena é usada para armazenar wrappers, mas você pode Vec<Box<Wrapper>> com o tipo Vec<Box<Wrapper>> , o principal é garantir que o Wrapper não se mova para lugar nenhum (durante a noite Rust, você pode usar a API de pinos para isso):


 struct TypeSystem { wrappers: typed_arena::Arena<Wrapper>, } impl TypeSystem { pub fn new() -> Self { Self { wrappers: typed_arena::Arena::new(), } } ///     `input`,      , ///    (  ,    , ///        )! pub fn annotate<'a: 'b, 'b>( &'a self, input: &'b String, type_name: &str ) -> &'b dyn Object { self.wrappers.alloc(Wrapper { value: input, type_name: type_name.into(), }) } } 

Mas para onde foi o parâmetro responsável pelo tempo de vida do link para o tipo Wrapper ? Tivemos que nos livrar dele, pois não podemos atribuir uma vida fixa ao tipo typed_arena::Arena<Wrapper<'?>> . Cada wrapper possui um parâmetro exclusivo, dependendo da input !


Em vez disso, espalhamos um pouco de Rust inseguro para se livrar do parâmetro de tempo de vida:


 struct Wrapper { value: *const String, type_name: String, } impl Object for Wrapper { fn type_name(&self) -> &str { &self.type_name } ///   -- ,     (  /// `annotate`),     (    - /// `&Object`)  ,      (`String`). fn as_string(&self) -> &String { unsafe { &*self.value } } } 

E os testes passam novamente, dando-nos confiança na exatidão da decisão. Além de se sentir desconfortável com o unsafe (como deveria ser, é melhor não brincar com o Rust inseguro!).


Mas ainda assim, e a opção prometida, que não requer alocações de memória adicionais para wrappers?


Abordagem nº 3: deixe os portões do inferno se abrirem


Ideia. Para cada "tipo" único ("Widget", "Gadget"), criaremos uma tabela virtual. Mãos durante a execução do programa. E nós o atribuímos ao link fornecido a nós pelos próprios dados (que, como lembramos, é simplesmente String ).


Primeiro, uma breve descrição do que precisamos obter. Então, uma referência a um objeto de tipo, como ele é organizado? De fato, esses são apenas dois ponteiros, um para os próprios dados e outro para a tabela virtual. Então escrevemos:


 #[repr(C)] struct TraitObject { pub data: *const (), pub vtable: *const (), } 

( #[repr(C)] precisamos garantir a localização correta na memória).


Parece que tudo é simples, vamos gerar uma nova tabela para os parâmetros fornecidos e "coletar" um link para o objeto de tipo! Mas em que consiste esta tabela?


A resposta correta para esta pergunta seria "este é um detalhe da implementação". Mas nós faremos isso; crie um arquivo rust-toolchain na raiz do nosso projeto e escreva-o lá: nightly-2018-12-01 . Afinal, uma montagem fixa pode ser considerada estável, certo?


Agora que corrigimos a versão Rust (de fato, precisaremos do assembly noturno para uma das bibliotecas abaixo).


Após algumas pesquisas na Internet , descobrimos que o formato da tabela é simples: primeiro há um link para o destruidor, depois dois campos associados à alocação de memória (tamanho e alinhamento do tipo) e, em seguida, as funções passam uma após a outra (a ordem fica a critério do compilador, mas temos apenas duas funções, então a probabilidade de adivinhar é bastante alta, 50%).


Então escrevemos:


 #[repr(C)] #[derive(Clone, Copy)] struct VirtualTableHeader { destructor_fn: fn(*mut ()), size: usize, align: usize, } #[repr(C)] struct ObjectVirtualTable { header: VirtualTableHeader, type_name_fn: fn(*const String) -> *const str, as_string_fn: fn(*const String) -> *const String, } 

Da mesma forma, #[repr(C)] necessário #[repr(C)] para garantir a localização correta na memória. Dividi-me em duas estruturas, um pouco mais tarde será útil para nós.


Agora vamos tentar escrever nosso sistema de tipos, que fornecerá a função de annotate . Nós precisaremos armazenar em cache as tabelas geradas, então vamos obter o cache:


 struct TypeInfo { vtable: ObjectVirtualTable, } #[derive(Default)] struct TypeSystem { infos: RefCell<HashMap<String, TypeInfo>>, } 

Usamos o estado interno do RefCell para que nossa função TypeSystem::annotate possa receber &self como um link compartilhado. Isso é importante, pois "emprestamos" do TypeSystem para garantir que as tabelas virtuais que geramos TypeSystem mais tempo do que a referência ao objeto de tipo que retornamos da annotate .


Como queremos poder anotar muitas instâncias, não podemos pedir emprestado &mut self como um link mutável.


E vamos esboçar este código:


 impl TypeSystem { pub fn annotate<'a: 'b, 'b>( &'a self, input: &'b String, type_name: &str ) -> &'b dyn Object { let type_name = type_name.to_string(); let mut infos = self.infos.borrow_mut(); let imp = infos.entry(type_name).or_insert_with(|| unsafe { //    ,  ? let vtable = unimplemented!(); TypeInfo { vtable } }); let object_obj = TraitObject { data: input as *const String as *const (), vtable: &imp.vtable as *const ObjectVirtualTable as *const (), }; //       - unsafe { std::mem::transmute::<TraitObject, &dyn Object>(object_obj) } } } 

De onde tiramos essa tabela? As três primeiras entradas nele corresponderão às entradas de qualquer outra tabela virtual para o tipo especificado. Portanto, basta pegar e copiá-los. Primeiro, vamos obter esse tipo:


 trait Whatever {} impl<T> Whatever for T {} 

É útil para nós obter essa mesma "qualquer outra tabela virtual". E então, copiamos essas três entradas dele:


 let whatever = input as &dyn Whatever; let whatever_obj = std::mem::transmute::<&dyn Whatever, TraitObject>(whatever); let whatever_vtable_header = whatever_obj.vtable as *const VirtualTableHeader; let vtable = ObjectVirtualTable { //  ! header: *whatever_vtable_header, type_name_fn: unimplemented!(), as_string_fn: unimplemented!(), }; TypeInfo { vtable } 

Em princípio, poderíamos obter o tamanho e o alinhamento através de std::mem::size_of::<String>() e std::mem::align_of::<String>() . Mas de onde mais o "destruidor" pode ser "roubado", eu não sei.


Ok, mas onde obtemos os endereços dessas funções, type_name_fn e as_string_fn ? Você pode perceber que as_string_fn não é necessário; o ponteiro de dados sempre é o primeiro registro na representação do objeto de tipo. Ou seja, esta função é sempre a mesma:


 impl Object for String { // ... fn as_string(&self) -> String { self } } 

Mas com a segunda função, não é tão fácil! Também depende do nosso nome "type", type_name .


Não importa, podemos apenas gerar essa função em tempo de execução. Vamos pegar a biblioteca do dynasm para isso (no momento, requer a construção noturna do Rust). Leia sobre
convenções de chamada de função .


Por uma questão de simplicidade, suponha que estamos interessados ​​apenas no Mac OS e Linux (depois de todas essas transformações divertidas, a compatibilidade não nos incomoda mais, certo?). E, sim, exclusivamente x86-64, é claro.


A segunda função, as_string , é fácil de implementar. Prometemos que o primeiro parâmetro estará no registro RDI . E retorne o valor para RAX . Ou seja, o código da função será algo como:


 dynasm!(ops ; mov rax, rdi ; ret ); 

Mas a primeira função é um pouco mais complicada. Primeiro, precisamos retornar &str , que é um ponteiro grosso. Sua primeira parte é um ponteiro para uma sequência e a segunda parte é o comprimento da fatia da sequência. Felizmente, a convenção acima permite retornar resultados de 128 bits usando o registro EDX para a segunda parte.


Resta encontrar um link para uma fatia de string que contenha nossa string type_name . Não queremos confiar no type_name (embora, por meio de anotações durante a vida útil, possamos garantir que o type_name durará mais do que o valor retornado).


Mas temos uma cópia dessa linha, que colocamos na tabela de hash. Cruzando os dedos, String::as_str que o local da fatia da string que String::as_str não String::as_str não muda o movimento da String (e a String será movida no processo de alteração do tamanho do HashMap que essa string é armazenada pela chave). Não sei se a biblioteca padrão garante esse comportamento, mas estamos simplificando?


Temos os componentes necessários:


 let type_name_ptr = type_name.as_str().as_ptr(); let type_name_len = type_name.as_str().len(); 

E escreva esta função:


 dynasm!(ops ; mov rax, QWORD type_name_ptr as i64 ; mov rdx, QWORD type_name_len as i64 ; ret ); 

E, finalmente, o código de annotate final:


 pub fn annotate<'a: 'b, 'b>(&'a self, input: &'b String, type_name: &str) -> &'b Object { let type_name = type_name.to_string(); //       let type_name_ptr = type_name.as_str().as_ptr(); let type_name_len = type_name.as_str().len(); let mut infos = self.infos.borrow_mut(); let imp = infos.entry(type_name).or_insert_with(|| unsafe { let mut ops = dynasmrt::x64::Assembler::new().unwrap(); //     `type_name` let type_name_offset = ops.offset(); dynasm!(ops ; mov rax, QWORD type_name_ptr as i64 ; mov rdx, QWORD type_name_len as i64 ; ret ); //     `as_string` let as_string_offset = ops.offset(); dynasm!(ops ; mov rax, rdi ; ret ); let buffer = ops.finalize().unwrap(); //      let whatever = input as &dyn Whatever; let whatever_obj = std::mem::transmute::<&dyn Whatever, TraitObject>(whatever); let whatever_vtable_header = whatever_obj.vtable as *const VirtualTableHeader; let vtable = ObjectVirtualTable { header: *whatever_vtable_header, type_name_fn: std::mem::transmute(buffer.ptr(type_name_offset)), as_string_fn: std::mem::transmute(buffer.ptr(as_string_offset)), }; TypeInfo { vtable, buffer } }); assert_eq!(imp.vtable.header.size, std::mem::size_of::<String>()); assert_eq!(imp.vtable.header.align, std::mem::align_of::<String>()); let object_obj = TraitObject { data: input as *const String as *const (), vtable: &imp.vtable as *const ObjectVirtualTable as *const (), }; unsafe { std::mem::transmute::<TraitObject, &dyn Object>(object_obj) } } 

Para fins de dynasm também precisamos adicionar o campo de buffer à nossa estrutura TypeInfo . Este campo controla a memória que armazena o código de nossas funções geradas:


 #[allow(unused)] buffer: dynasmrt::ExecutableBuffer, 

E todos os testes passam!


Feito, mestre!


Tão fácil e naturalmente você pode gerar sua própria implementação de objetos de tipo no código Rust!


A última solução depende ativamente dos detalhes da implementação e, portanto, não é recomendada para uso. Mas, na realidade, você precisa fazer o que precisa. Tempos desesperados exigem medidas desesperadas!


Há, no entanto, um (mais) recurso que eu confio aqui. Ou seja, é seguro liberar a memória ocupada virtualmente pela tabela depois que não houver referências ao objeto de tipo que a utiliza. Por um lado, é lógico que você possa usar uma tabela virtual apenas através de referências de objetos de tipo. Por outro lado, as tabelas fornecidas pelo Rust têm uma vida útil 'static . É inteiramente possível assumir algum código que separará a tabela do link para alguns de seus propósitos (você nunca sabe, por exemplo, alguns de seus truques sujos ).


O código fonte pode ser encontrado aqui .

Source: https://habr.com/ru/post/pt432202/


All Articles