Quod erat demonstrandum
Este tópico vai ser mais curtinho que o normal, queria mais deixar registrado aqui um negócio que descobri enquanto fuçava em coisas para colocar no script que estou fazendo pro Colosseum.
O da vez é uma classe para criar callbacks escritos em Ruby para funções C.
O problema
Algumas funções nativas (da Win32 API, por exemplo) recebem ponteiros de função como parâmetro. Esses ponteiros costumam ser usados como forma de callback, isto é, uma função chamada quando outra termina ou chega em determinado ponto em sua execução que pode ser interessante de fora dela. Exemplos de funções com callbacks são a EnumWindows ou o loop de eventos das janelas, que chama um WindowProc definido pelo usuário quando recebe um evento.
O problema é que esses ponteiros de função devem ser ponteiros de funções nativas. Sendo assim, não podemos fazer essas funções só com scripts: acaba sendo necessário criar uma DLL ou um programa com a função e pegar o ponteiro dela de lá. Isso não é muito flexível, e é meio chatinho de fazer (ter que compilar uma DLL pra mudar o script, eca); além disso, não é possível chamar Ruby de dentro dessas funções nativas, porque o RPG Maker não expõe a VM do ruby.
A solução
De forma simplificada, gerar funções nativas usando Ruby.
O processo é mais ou menos assim:
Mais difícil ainda, eu descobri, é converter as instruções que você escreve do Assembly em um programa de fato. A codificação dos comandos é bem complicada, e é fácil de se perder nas instruções, suas variações e seus parâmetros. Por sorte, existem assemblers online que fazem isso pra gente. Claro, poderíamos fazer isso com um compilador normal, depois abrir num editor hexadecimal ou mesmo um bom debugger e ver o código que foi gerado, mas dá bem mais trabalho.
Pra facilitar ainda mais, eu busquei deixar a função em assembly bem enxuta e passar o comando pro RGSS assim que possível. Para isso, a função assembly só manda os parâmetros que recebe para locais de memória (que o RGSS consegue acessar, usando a classe DL::CPtr) e depois chama a função RGSSEval com uma chamada especial. Além disso, pra permitir valores de retorno nas funções de callback (algumas funções precisam), usei um ponteiro para guardar o valor de retorno da função Ruby, que o assembly pega e coloca no registrador de retorno depois; dessa forma, é como se a função retornasse o que o callback ruby retornou para a função nativa.
Enfim, sem mais enrolar, o código da função assembly ficou mais ou menos assim:
Tem algumas partes de sintaxe que eu inventei aí, mas é pra ficar mais fácil de entender o que acontece de fato na função. Essa função não está escrita toda no script desse jeito, ela é meio "compilada" em tempo real.
Agora, sim, o script que permite fazer essas coisas:
Exemplos
Essa é a parte legal. Será que funciona? Veremos.
Primeiro, um exemplo bem bobinho, somando dois números:
Explicando:
Um exemplo um pouco mais elaborado:
Executando o jogo com esse script, você deve ver o nome de cada janela no seu desktop (algumas, inclusive, que são invisíveis). Louco né?
Alguns problemas
Como nem tudo são flores, essa solução não dá conta de tudo. Um exemplo que testei e vi que não dá certo, por exemplo, são funções assíncronas. Não dá pra criar threads com isso, infelizmente.
Na verdade, possível até é: só não tem como chamar a função do Ruby. Colocar o argumentos da função nos buffers intermediários, por exemplo, pode ser feito de forma assíncrona, embora eu acredite que provavelmente também não de forma segura. Isso provavelmente acontece porque o RGSSEval não tem os mecanismos de sincronização que seriam necessários para que isso desse certo. Vou estudar um pouco mais o assunto e tentar achar solução pra isso, mas é possível que não seja viável mesmo.
Outro problema nessa implementação é que não é possível criar funções com argumentos variádicos. Esse problema é mais por conta da forma como o assembly foi escrito, e deve dar pra resolver, só preciso estudar mais, de novo xd.
Bom, era isso que eu queria apresentar hoje. Fiz isso aí meio que na expectativa de usar em algum ponto do script no colosseum, mas acabei não precisando pro que achei que precisaria. Como é um negócio que acho que alguns já passaram por dificuldade para fazer e que pode ser bem útil em alguns casos, imaginei que seria legal compartilhar.
Obrigado pela atenção \o
O da vez é uma classe para criar callbacks escritos em Ruby para funções C.
O problema
Algumas funções nativas (da Win32 API, por exemplo) recebem ponteiros de função como parâmetro. Esses ponteiros costumam ser usados como forma de callback, isto é, uma função chamada quando outra termina ou chega em determinado ponto em sua execução que pode ser interessante de fora dela. Exemplos de funções com callbacks são a EnumWindows ou o loop de eventos das janelas, que chama um WindowProc definido pelo usuário quando recebe um evento.
O problema é que esses ponteiros de função devem ser ponteiros de funções nativas. Sendo assim, não podemos fazer essas funções só com scripts: acaba sendo necessário criar uma DLL ou um programa com a função e pegar o ponteiro dela de lá. Isso não é muito flexível, e é meio chatinho de fazer (ter que compilar uma DLL pra mudar o script, eca); além disso, não é possível chamar Ruby de dentro dessas funções nativas, porque o RPG Maker não expõe a VM do ruby.
A solução
De forma simplificada, gerar funções nativas usando Ruby.
O processo é mais ou menos assim:
- Alocar um espaço de memória para colocar as instruções e permitir execução desse espaço de memória
- Preencher esse espaço com instruções ASM x86 (i.e. linguagem de máquina, o x86 é a arquitetura; os jogos de RPG Maker todos usam x86, ou seja, têm ponteiros de 32 bits). Mais sobre isso daqui a pouco.
Mais difícil ainda, eu descobri, é converter as instruções que você escreve do Assembly em um programa de fato. A codificação dos comandos é bem complicada, e é fácil de se perder nas instruções, suas variações e seus parâmetros. Por sorte, existem assemblers online que fazem isso pra gente. Claro, poderíamos fazer isso com um compilador normal, depois abrir num editor hexadecimal ou mesmo um bom debugger e ver o código que foi gerado, mas dá bem mais trabalho.
Pra facilitar ainda mais, eu busquei deixar a função em assembly bem enxuta e passar o comando pro RGSS assim que possível. Para isso, a função assembly só manda os parâmetros que recebe para locais de memória (que o RGSS consegue acessar, usando a classe DL::CPtr) e depois chama a função RGSSEval com uma chamada especial. Além disso, pra permitir valores de retorno nas funções de callback (algumas funções precisam), usei um ponteiro para guardar o valor de retorno da função Ruby, que o assembly pega e coloca no registrador de retorno depois; dessa forma, é como se a função retornasse o que o callback ruby retornou para a função nativa.
Enfim, sem mais enrolar, o código da função assembly ficou mais ou menos assim:
Código:
# Guarda o valor de EBP e substitui por ESP
push ebp
mov ebp, esp
# Transfere os parâmetros recebidos da pilha para os ponteiros intermediários
mov eax, [ebp+8]
mov [<args[0]>], eax
mov eax, [ebp+12]
mov [<args[1]>], eax
....
mov eax, [ebp+8+4n]
mov [<args[n]>], eax
# Recupera o antigo valor de EBP
pop ebp
# Adiciona a string a ser executado pelo RGSSEval à pilha, como argumento
mov eax, <ruby_eval_string>
push eax
# Chama a função RGSSEval
mov ecx, <RGSS301.dll:RGSSEval>
call ecx
# Consome um valor da pilha. É curioso que sem isso ele quebra, mas não deveria.
# Isso indica que a função RGSSEval não está consumindo toda a pilha, ou colocando
# algo que não deveria nela. Ou eu que estou viajando.
add esp, 4
# Salva em EAX (o registrador de retorno) o valor retornado pela função ruby
# A chama à função RGSSEval vai colocar o valor apropriado nesse endereço de memória
mov eax, [ruby_return_value]
# Retorna e limpa a pilha
ret 4n # n = número de argumentos da função
Tem algumas partes de sintaxe que eu inventei aí, mas é pra ficar mais fácil de entender o que acontece de fato na função. Essa função não está escrita toda no script desse jeito, ela é meio "compilada" em tempo real.
Agora, sim, o script que permite fazer essas coisas:
Ruby:
#==============================================================================
# ** Ini
#------------------------------------------------------------------------------
# Este módulo implementa lógica de leitura de arquivos .ini.
#==============================================================================
module Ini
#--------------------------------------------------------------------------
# * Funções da Win32 API
#--------------------------------------------------------------------------
GetPrivateProfileString = Win32API.new('kernel32',
'GetPrivateProfileStringA',
'pppplp', 'l')
#--------------------------------------------------------------------------
# * Obtém um valor do arquivo
# file : Nome do arquivo
# section : Seção do valor no arquivo
# key : Chave do valor na seção
# default : Valor padrão retornado caso a chave não exista (opcional)
#--------------------------------------------------------------------------
def self.get(file, section, key, default = nil)
unless FileTest.file?(file)
raise "`#{file}' does not exist or is not a file"
end
buffer = Array.new(256, 0).pack('C*')
length = Ini::GetPrivateProfileString.(
section,
key,
default || 0,
buffer,
256,
file)
return buffer[0, length]
end
end
#==============================================================================
# ** CFunction
#------------------------------------------------------------------------------
# Classe para uma função nativa com callback para Ruby.
#==============================================================================
class CFunction
#--------------------------------------------------------------------------
# * Constantes
#--------------------------------------------------------------------------
MEM_COMMIT = 0x00001000
PAGE_READWRITE = 0x00000004
PAGE_EXECUTE_READ = 0x00000020
RGSSEval = DL.dlopen(Ini.get('./Game.ini', 'Game', 'Library'))['RGSSEval']
#--------------------------------------------------------------------------
# * Funções da Win32 API
#--------------------------------------------------------------------------
VirtualAlloc = Win32API.new('kernel32', 'VirtualAlloc', 'plll', 'l')
CopyMemory = Win32API.new('kernel32', 'RtlMoveMemory', 'lpl', 'v')
VirtualProtect = Win32API.new('kernel32', 'VirtualProtect', 'pllp', 'i')
VirtualFree = Win32API.new('kernel32', 'VirtualFree', 'pll', 'i')
#--------------------------------------------------------------------------
# * Inicialização do objeto
#--------------------------------------------------------------------------
def initialize(args, &block)
@args = args
@arg_buffers = Array.new(args.size) do
VirtualAlloc.(0, 4, MEM_COMMIT, PAGE_READWRITE)
end
@return_buffer = VirtualAlloc.(0, 4, MEM_COMMIT, PAGE_READWRITE)
@callback = block
compile
end
#--------------------------------------------------------------------------
# * Compila a função
#--------------------------------------------------------------------------
def compile
@callback_script = "ObjectSpace._id2ref(#{self.object_id}).call"
tmp = [0x55].pack('C*')
tmp << [0x89, 0xE5].pack('C*')
@args.chars.each_with_index do |t, i|
tmp << [0x8B, 0x45, 4 * (i + 2)].pack('C*')
tmp << [0xA3, @arg_buffers[i]].pack('CL')
end
tmp << [0x5D].pack('C*')
tmp << [0xB8, DL::CPtr[@callback_script].to_i].pack('CL')
tmp << [0x50].pack('C*')
tmp << [0xBA, RGSSEval].pack('CL')
tmp << [0xFF, 0xD2].pack('C*')
tmp << [0x83, 0xC4, 0x04].pack('C*')
tmp << [0xA1, @return_buffer].pack('CL')
tmp << [0xC2, @args.size * 4].pack('CS')
@pointer = VirtualAlloc.(0, tmp.size, MEM_COMMIT, PAGE_READWRITE)
CopyMemory.(@pointer, tmp, tmp.size)
dummy = [0].pack('L')
VirtualProtect.(@pointer, tmp.size, PAGE_EXECUTE_READ, dummy)
end
#--------------------------------------------------------------------------
# * Chama a função
#--------------------------------------------------------------------------
def call
ret = @callback.call(*@arg_buffers.each_with_index.map do |pointer, i|
type = @args[i]
case type.upcase
when 'L', 'I', 'N', 'S', 'C'
DL::CPtr.new(pointer).to_s(4).unpack(type).first
when 'P'
DL::CPtr.new(pointer).to_s
end
end)
ret = 1 if ret.is_a?(TrueClass)
ret = 0 if ret.is_a?(FalseClass)
ret_size = ret.is_a?(String) ? ret.size : 4
ret = [ret].pack('L') if ret.is_a?(Integer)
CopyMemory.(@return_buffer, ret, ret_size)
end
#--------------------------------------------------------------------------
# * Ponteiro da função
#--------------------------------------------------------------------------
def pointer
DL::CPtr[@pointer].to_i
end
end
Exemplos
Essa é a parte legal. Será que funciona? Veremos.
Primeiro, um exemplo bem bobinho, somando dois números:
Ruby:
sum = CFunction.new('ll') do |a, b|
a + b
end
puts DL::CFunc.new(sum.pointer, DL::TYPE_LONG).([2, 3])
Explicando:
- Criamos uma CFunction (a nossa classe de função Ruby que funciona como função nativa) que recebe dois argumentos inteiros (por isso o 'll'; as letras têm o mesmo significado que têm no método String#unpack). A declaração do corpo da função se dá por meio de um block passado no construtor, e que recebe dois argumentos (assim como especificado na função)
- Então, criamos um objeto DL::CFunc, que chama funções nativas a partir de um ponteiro (é a mesma classe usada internamente pelo módulo Win32API). Tirando a parte de sintaxe que é meio estranha, acho que fica claro o que fazemos com ele então.
Um exemplo um pouco mais elaborado:
Ruby:
EnumWindows = Win32API.new('user32', 'EnumWindows', 'll', 'i')
GetWindowText = Win32API.new('user32', 'GetWindowTextA', 'lpi', 'i')
enum_windows_proc = CFunction.new('LL') do |hwnd, lparam|
buffer = Array.new(0, 256).pack('C*')
n = GetWindowText.(hwnd, buffer, 256)
next true if n.zero?
msgbox buffer
true
end
EnumWindows.(enum_windows_proc.pointer, 0)
Executando o jogo com esse script, você deve ver o nome de cada janela no seu desktop (algumas, inclusive, que são invisíveis). Louco né?
Alguns problemas
Como nem tudo são flores, essa solução não dá conta de tudo. Um exemplo que testei e vi que não dá certo, por exemplo, são funções assíncronas. Não dá pra criar threads com isso, infelizmente.
Na verdade, possível até é: só não tem como chamar a função do Ruby. Colocar o argumentos da função nos buffers intermediários, por exemplo, pode ser feito de forma assíncrona, embora eu acredite que provavelmente também não de forma segura. Isso provavelmente acontece porque o RGSSEval não tem os mecanismos de sincronização que seriam necessários para que isso desse certo. Vou estudar um pouco mais o assunto e tentar achar solução pra isso, mas é possível que não seja viável mesmo.
Outro problema nessa implementação é que não é possível criar funções com argumentos variádicos. Esse problema é mais por conta da forma como o assembly foi escrito, e deve dar pra resolver, só preciso estudar mais, de novo xd.
Bom, era isso que eu queria apresentar hoje. Fiz isso aí meio que na expectativa de usar em algum ponto do script no colosseum, mas acabei não precisando pro que achei que precisaria. Como é um negócio que acho que alguns já passaram por dificuldade para fazer e que pode ser bem útil em alguns casos, imaginei que seria legal compartilhar.
Obrigado pela atenção \o