m3m0r7 / rubyvm-on-php
Requires
- php: >=8.2
- monolog/monolog: ^3.4
- symfony/console: ^6.3
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.21
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^10.2
- rector/rector: ^0.18.3
- squizlabs/php_codesniffer: ^3.7
README
PHP上的RubyVM是完全用PHP编写的RubyVM实现,100%。没有完全的文档说明如何实现RubyVM,我在贡献这个项目时参考了Ruby源代码。
注意:这个项目是一个非常超超超超实验性的实现
注意:我只测试了Ruby版本3.2和3.3
另请参阅
- https://github.com/ruby/ruby/blob/master/compile.c
- https://github.com/ruby/ruby/blob/master/vm.c
- https://github.com/ruby/ruby/blob/master/vm_exec.c
DEMO
需求
- PHP 8.2+
当前状态
- 实现了通用语法(定义局部变量、全局变量、类、方法、布尔值、哈希表、数组等)
- 实现了算术运算(+、-、*、/),位运算(|、&、<<、>>),一些运算符(**、%)并可重载
- 实现了块语法(
[].each do | var | ... end
)和非块语法([].push
) - 实现了方法调用时的关键字参数(
keyword_argument(a: "Hello", c: "!", b: "World")
) - 实现了使用数组和调用方法时的可变参数(
[*var1, *var2]
,keyword_argument(a, b, *c)
) - 实现了部分Ruby方法(
to_s
、to_i
、[].push
、foobar.nil?
) - 实现了case-when语法
- 实现了正则表达式语法(
p /Hello/ =~ "Hello World"
) - 实现了raise/rescue
- 等等(见测试目录)
快速开始
- 按以下方式使用composer安装
$ composer require m3m0r7/rubyvm-on-php
- 将以下代码保存为
HelloWorld.rb
puts RubyVM::InstructionSequence.compile("puts 'HelloWorld!\n'", "HelloWorld.rb").to_binary
- 按以下方式输出
.yarv
文件
$ ruby HelloWorld.rb > HelloWorld.yarv
- 创建以下代码的PHP文件并保存为
HelloWorld.php
<?php require __DIR__ . '/vendor/autoload.php'; // Instantiate RubyVM class $rubyVM = new \RubyVM\VM\Core\Runtime\RubyVM( new \RubyVM\VM\Core\Runtime\Option( reader: new \RubyVM\Stream\BinaryStreamReader( streamHandler: new \RubyVM\Stream\FileStreamHandler( // Specify to want you to load YARV file __DIR__ . '/HelloWorld.yarv', ), ), // Choose Logger logger: new \Psr\Log\NullLogger(), ), ); // Disassemble instruction sequence binary formatted and get executor $executor = $rubyVM->disassemble(); // You can choose to run ruby version if you needed // $executor = $rubyVM->disassemble( // useVersion: \RubyVM\RubyVersion::VERSION_3_2, // ); // Execute disassembled instruction sequence $executor->execute();
- 运行
php HelloWorld.php
,你将得到RubyVM输出的HelloWorld!
在PHP上调用定义的ruby方法
- 创建以下ruby代码
def callFromPHP puts "Hello World from Ruby!" end
然后,将文件保存为 test.rb
- 按以下方式编译到YARV
$ ruby -e "puts RubyVM::InstructionSequence.compile_file('test.rb').to_binary" > test.yarv
- 按以下方式在PHP上调用ruby方法
<?php require __DIR__ . '/vendor/autoload.php'; // Instantiate RubyVM class $rubyVM = new \RubyVM\VM\Core\Runtime\RubyVM( new \RubyVM\VM\Core\Runtime\Option( reader: new \RubyVM\Stream\BinaryStreamReader( streamHandler: new \RubyVM\Stream\FileStreamHandler( // Specify to want you to load YARV file __DIR__ . '/test.yarv', ), ), // Choose Logger logger: new \Psr\Log\NullLogger(), ), ); // Disassemble instruction sequence binary formatted and get executor $executor = $rubyVM->disassemble(); // Execute disassembled instruction sequence $executed = $executor->execute(); // Call Ruby method as below code. // In this case, you did define method name is `callFromPHP`. $executed->context()->callFromPHP();
你将得到输出 Hello World from Ruby!
。此外,在这种情况下,你可能想传递参数。当然,这是可用的。第一次,修改前面的代码如下。
def callFromPHP(text) puts text end
第二次,修改PHP代码 $executed->context()->callFromPHP()
如下
$executed->context()->callFromPHP('Hello World! Here is passed an argument from PHP!')
你将得到输出 Hello World! Here is passed an argument from PHP
。
使用执行器调试器
RubyVM on PHP提供了一个执行器调试器,可以将处理过的INSN和其他信息以表格形式显示如下
+-----+--------------------------------+-------------------------------+--------------------------------+--------------+
| PC | CALLEE | INSN | CURRENT STACKS | LOCAL TABLES |
+-----+--------------------------------+-------------------------------+--------------------------------+--------------+
| 0 | <main> | [0x12] putself | | |
+-----+--------------------------------+-------------------------------+--------------------------------+--------------+
| 1 | <main> | [0x15] putstring | Main#0 | |
+-----+--------------------------------+-------------------------------+--------------------------------+--------------+
| 3 | <main> | [0x33] opt_send_without_block | Main#0, String(Hello World!)#1 | |
| | | (Main#puts(Hello World!)) | | |
+-----+--------------------------------+-------------------------------+--------------------------------+--------------+
| 5 | <main> | [0x3c] leave | Nil(nil)#0 | |
+-----+--------------------------------+-------------------------------+--------------------------------+--------------+
如果你想要显示上述表格,请从快速开始中添加以下代码。
注意:执行器调试器会消耗大量内存。我们建议通常禁用。根据具体情况,可能需要使用-d memory_limit=NEEDING_MEMORY_BYTES
参数在调用php
命令时使其正常工作
// You can display processed an INSN table when adding below code $executor->context()->option()->debugger()->showExecutedOperations();
逐步调试
RubyVM on PHP提供了一个逐步调试器。它可以逐步确认处理过程。它会收集之前的堆栈、注册的本地表等信息。这对于调试这个项目是必要的。
// Use breakpoint debugger with option $rubyVM = new \RubyVM\VM\Core\Runtime\RubyVM( new \RubyVM\VM\Core\Runtime\Option( // excluded... debugger: new \RubyVM\VM\Core\Runtime\Executor\Debugger\StepByStepDebugger(), ), );
当你启用它时,将显示如下
+-----+--------------------------------+-------------------------------+--------------------------------+--------------+
| PC | CALLEE | INSN | CURRENT STACKS | LOCAL TABLES |
+-----+--------------------------------+-------------------------------+--------------------------------+--------------+
| 0 | <main> | [0x12] putself | | |
+-----+--------------------------------+-------------------------------+--------------------------------+--------------+
| 1 | <main> | [0x15] putstring | Main#0 | |
+-----+--------------------------------+-------------------------------+--------------------------------+--------------+
Current INSN: putstring(0x15)
Previous Stacks: [total: 1, OperandEntry<Main>]#966
Previous Local Tables: []
Current Stacks: [total: 2, OperandEntry<Main>, OperandEntry<StringSymbol@HelloWorld!>]#561
Current Local Tables: []
Enter to next step (y/n/q): <INPUT_YOU_EXPECTING_NEXT_STEP>
自定义方法
RubyVM on PHP在主上下文中具有自定义方法。尝试在RubyVM on PHP上使用以下Ruby代码调用phpinfo
phpinfo
然后你将看到显示PHP Version: 8.2.7
测试
$ ./vendor/bin/phpunit tests/
代码检查器
./vendor/bin/php-cs-fixer fix --allow-risky=yes
如何贡献
- 使用
-DIBF_ISEQ_DEBUG
标志从源代码构建你的ruby环境
参见: https://docs.ruby-lang.cn/en/master/contributing/building_ruby_md.html
$ git clone git@github.com:ruby/ruby.git
$ mkdir build && cd build
$ ../configure cppflags="-DIBF_ISEQ_DEBUG=1"
$ make -j$(nproc)
-
当您构建 Ruby 环境时,将会得到一个
vm.inc
文件,该文件记录了如何执行每个 INSN 命令 -
当按照以下方式运行 Ruby 代码时,您可以在
ibf_load_**
获取日志
...omitted
ibf_load_object: type=0x15 special=1 frozen=1 internal=1 // The type is a FIX_NUMBER (2)
ibf_load_object: index=0x3 obj=0x5
ibf_load_object: list=0xf0 offsets=0x12b80fcf0 offset=0xe1
ibf_load_object: type=0x15 special=1 frozen=1 internal=1 // The type is a FIX_NUMBER (3)
ibf_load_object: index=0x4 obj=0x7
ibf_load_object: list=0xf0 offsets=0x12b80fcf0 offset=0xcd
ibf_load_object: type=0x5 special=0 frozen=1 internal=0 // The type is a STRING SYMBOL (puts)
...omitted
上述日志是由以下示例代码创建的
puts 1 + 2 + 3
- 参考它,现在您可以贡献于在 PHP 上的 RubyVM 实现INSN 命令
我的其他玩具
- PHPJava - 使用 PHP 实现的 JVM
- nfc-for-php - 使用 PHP 编写的 NFC 读取器(控制 NFC 硬件)
- PHPPython - 使用 PHP 实现的 PYC 执行器