mkxp-z/retro/sandbox-bindgen.rb

583 lines
20 KiB
Ruby

# sandbox-bindgen.rb
#
# This file is part of mkxp.
#
# Copyright (C) 2013 - 2021 Amaryllis Kulla <ancurio@mapleshrine.eu>
#
# mkxp is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# mkxp is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with mkxp. If not, see <http://www.gnu.org/licenses/>.
################################################################################
# True if generating bindings for 64-bit WebAssembly, false if generating bindings for 32-bit WebAssembly
MEMORY64 = false
# The name passed as the `-n`/`--module-name` flag to `wasm2c`
MODULE_NAME = 'ruby'
# Include directive for including the header file generated by `wasm2c`
MODULE_INCLUDE = '#include <mkxp-retro-ruby.h>'
# The name of the `malloc()` binding defined in ruby-bindings.h
MALLOC_FUNC = 'mkxp_sandbox_malloc'
# The name of the `free()` binding defined in ruby-bindings.h
FREE_FUNC = 'mkxp_sandbox_free'
FIBER_ENTRY_POINT_FUNC = 'mkxp_sandbox_fiber_entry_point'
FIBER_ARG0_FUNC = 'mkxp_sandbox_fiber_arg0'
FIBER_ARG1_FUNC = 'mkxp_sandbox_fiber_arg1'
################################################################################
IGNORED_FUNCTIONS = Set[
'rb_class_descendants',
'rb_close_before_exec',
]
ARG_HANDLERS = {
'VALUE' => { keep: true, primitive: :size },
'ID' => { keep: true, primitive: :size },
'int' => { primitive: :s32 },
'unsigned int' => { primitive: :u32 },
'long' => { primitive: :size },
'unsigned long' => { primitive: :size },
'long long' => { primitive: :s64 },
'unsigned long long' => { primitive: :u64 },
'const char *' => {
keep: true,
buf_size: 'std::strlen(ARG) + 1',
serialize: "std::strcpy((char *)(bind.instance->w2c_memory.data + BUF), ARG);\n",
},
'const VALUE *' => {
keep: true,
condition: lambda { |func_name, args, arg_index| arg_index > 0 && args[arg_index - 1] == 'int' }, # Only handle arguments of type `const VALUE *` if the previous argument is of type `int`
buf_size: 'PREV_ARG * sizeof(VALUE)',
serialize: <<~HEREDOC
for (int i = 0; i < PREV_ARG; ++i) {
((VALUE *)(bind.instance->w2c_memory.data + BUF))[i] = SERIALIZE_PTR(ARG[i]);
}
HEREDOC
},
'VALUE (*)()' => {
keep: true,
anyargs: true,
formatter: lambda { |name| "VALUE (*#{name})(void *, ANYARGS)" },
declaration: 'VALUE (*)(void *, ANYARGS)',
},
'VALUE (*)(VALUE)' => {
keep: true,
func_ptr_args: [:size],
func_ptr_rets: [:size],
formatter: lambda { |name| "VALUE (*#{name})(void *, VALUE)" },
declaration: 'VALUE (*)(void *, VALUE)',
},
'VALUE (*)(VALUE,VALUE)' => {
keep: true,
func_ptr_args: [:size, :size],
func_ptr_rets: [:size],
formatter: lambda { |name| "VALUE (*#{name})(void *, VALUE, VALUE)" },
declaration: 'VALUE (*)(void *, VALUE, VALUE)',
},
}
RET_HANDLERS = {
'void' => { keep: true, primitive: :void },
'VALUE' => { keep: true, primitive: :size },
'ID' => { keep: true, primitive: :size },
'int' => { primitive: :s32 },
'unsigned int' => { primitive: :u32 },
'long' => { primitive: :size },
'unsigned long' => { primitive: :size },
'long long' => { primitive: :s64 },
'unsigned long long' => { primitive: :u64 },
}
VAR_TYPE_TABLE = {
ssize: 'wasm_ssize_t',
size: 'wasm_size_t',
ptr: 'wasm_ptr_t',
s32: 'int32_t',
u32: 'uint32_t',
s64: 'int64_t',
u64: 'uint64_t',
f32: 'float',
f64: 'double',
}
FUNC_TYPE_TABLE = {
ssize: MEMORY64 ? 'WASM_RT_I64' : 'WASM_RT_I32',
size: MEMORY64 ? 'WASM_RT_I64' : 'WASM_RT_I32',
ptr: MEMORY64 ? 'WASM_RT_I64' : 'WASM_RT_I32',
s32: 'WASM_RT_I32',
u32: 'WASM_RT_I32',
s64: 'WASM_RT_I64',
u64: 'WASM_RT_I64',
f32: 'WASM_RT_F32',
f64: 'WASI_RT_F64',
}
################################################################################
HEADER_START = <<~HEREDOC
/*
** mkxp-sandbox-bindgen.h
**
** This file is part of mkxp.
**
** Copyright (C) 2013 - 2021 Amaryllis Kulla <ancurio@mapleshrine.eu>
**
** mkxp is free software: you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation, either version 2 of the License, or
** (at your option) any later version.
**
** mkxp is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with mkxp. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef MKXP_SANDBOX_BINDGEN_H
#define MKXP_SANDBOX_BINDGEN_H
#include <cstdint>
#include <cstring>
#include <memory>
#include <unordered_map>
#include <vector>
#include <boost/container_hash/hash.hpp>
#include <boost/any.hpp>
#include <boost/asio/coroutine.hpp>
#include <boost/asio/yield.hpp>
#{MODULE_INCLUDE}
#include "binding-sandbox/types.h"
// Autogenerated by sandbox-bindgen.rb. Don't manually modify this file - modify sandbox-bindgen.rb instead!
#define ANYARGS ...
typedef int#{MEMORY64 ? '64' : '32'}_t wasm_ssize_t;
typedef uint#{MEMORY64 ? '64' : '32'}_t wasm_size_t;
typedef wasm_size_t wasm_ptr_t;
typedef wasm_size_t VALUE;
typedef wasm_size_t ID;
namespace mkxp_sandbox {
struct bindings {
private:
typedef std::tuple<wasm_ptr_t, wasm_ptr_t, wasm_ptr_t> key_t;
struct fiber {
key_t key;
std::vector<boost::any> stack;
size_t stack_ptr;
};
wasm_ptr_t next_func_ptr;
std::shared_ptr<struct w2c_#{MODULE_NAME}> instance;
std::unordered_map<key_t, struct fiber, boost::hash<key_t>> fibers;
wasm_ptr_t sbindgen_malloc(wasm_ptr_t);
wasm_ptr_t sbindgen_create_func_ptr();
public:
bindings(std::shared_ptr<struct w2c_#{MODULE_NAME}>);
template <typename T> struct stack_frame {
friend struct bindings;
private:
struct bindings &bind;
struct fiber &fiber;
T &inner;
static inline struct fiber &init_fiber(struct bindings &bind) {
key_t key = {
w2c_#{MODULE_NAME}_#{FIBER_ENTRY_POINT_FUNC}(bind.instance.get()),
w2c_#{MODULE_NAME}_#{FIBER_ARG0_FUNC}(bind.instance.get()),
w2c_#{MODULE_NAME}_#{FIBER_ARG1_FUNC}(bind.instance.get()),
};
if (bind.fibers.count(key) == 0) {
bind.fibers[key] = (struct fiber){.key = key};
}
return bind.fibers[key];
}
static inline T &init_inner(struct bindings &bind, struct fiber &fiber) {
if (fiber.stack_ptr == fiber.stack.size()) {
fiber.stack.push_back(T(bind));
} else if (fiber.stack_ptr > fiber.stack.size()) {
throw SandboxTrapException();
}
try {
T &inner = boost::any_cast<T &>(fiber.stack[fiber.stack_ptr]);
++fiber.stack_ptr;
return inner;
} catch (boost::bad_any_cast &) {
fiber.stack.resize(fiber.stack_ptr++);
fiber.stack.push_back(T(bind));
return boost::any_cast<T &>(fiber.stack.back());
}
}
stack_frame(struct bindings &b) : bind(b), fiber(init_fiber(b)), inner(init_inner(b, fiber)) {}
public:
~stack_frame() {
if (inner.is_complete()) {
fiber.stack.pop_back();
}
--fiber.stack_ptr;
if (fiber.stack.empty()) {
bind.fibers.erase(fiber.key);
}
}
inline T &operator()() {
return inner;
}
};
template <typename T> struct stack_frame<T> bind() {
return (struct stack_frame<T>)(*this);
}
HEREDOC
HEADER_END = <<~HEREDOC
}
#endif // MKXP_SANDBOX_BINDGEN_H
HEREDOC
PRELUDE = <<~HEREDOC
/*
** mkxp-sandbox-bindgen.cpp
**
** This file is part of mkxp.
**
** Copyright (C) 2013 - 2021 Amaryllis Kulla <ancurio@mapleshrine.eu>
**
** mkxp is free software: you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation, either version 2 of the License, or
** (at your option) any later version.
**
** mkxp is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with mkxp. If not, see <http://www.gnu.org/licenses/>.
*/
// Autogenerated by sandbox-bindgen.rb. Don't manually modify this file - modify sandbox-bindgen.rb instead!
#include <cstdarg>
#include "mkxp-sandbox-bindgen.h"
#if WABT_BIG_ENDIAN
#define SERIALIZE_32(value) __builtin_bswap32(value)
#define SERIALIZE_64(value) __builtin_bswap64(value)
#else
#define SERIALIZE_32(value) (value)
#define SERIALIZE_64(value) (value)
#endif
#define SERIALIZE_PTR(value) SERIALIZE_#{MEMORY64 ? '64' : '32'}(value)
using namespace mkxp_sandbox;
bindings::bindings(std::shared_ptr<struct w2c_#{MODULE_NAME}> m) : next_func_ptr(-1), instance(m) {}
wasm_ptr_t bindings::sbindgen_malloc(wasm_size_t size) {
wasm_ptr_t buf = w2c_#{MODULE_NAME}_#{MALLOC_FUNC}(instance.get(), size);
// Verify that the entire allocated buffer is in valid memory
wasm_ptr_t buf_end;
if (buf == 0 || __builtin_add_overflow(buf, size, &buf_end) || buf_end >= instance->w2c_memory.size) {
return 0;
}
return buf;
}
wasm_ptr_t bindings::sbindgen_create_func_ptr() {
if (next_func_ptr == (wasm_ptr_t)-1) {
next_func_ptr = instance->w2c_T0.size;
}
if (next_func_ptr < instance->w2c_T0.max_size) {
return next_func_ptr++;
}
// Make sure that an integer overflow won't occur if we double the max size of the funcref table
wasm_size_t new_max_size;
if (__builtin_add_overflow(instance->w2c_T0.max_size, instance->w2c_T0.max_size, &new_max_size)) {
return -1;
}
// Double the max size of the funcref table
wasm_size_t old_max_size = instance->w2c_T0.max_size;
instance->w2c_T0.max_size = new_max_size;
// Double the size of the funcref table buffer
if (wasm_rt_grow_funcref_table(&instance->w2c_T0, old_max_size, wasm_rt_funcref_t {
.func_type = wasm2c_ruby_get_func_type(0, 0),
.func = NULL,
.func_tailcallee = {.fn = NULL},
.module_instance = instance.get(),
}) != old_max_size) {
instance->w2c_T0.max_size = old_max_size;
return -1;
}
return next_func_ptr++;
}
//////////////////////////////////////////////////////////////////////////////
HEREDOC
################################################################################
declarations = []
coroutines = []
func_names = []
globals = []
File.readlines('tags', chomp: true).each do |line|
line = line.split("\t")
next unless line[3] == 'x'
global_name = line[0]
next unless global_name.match?(/^rb_[a-z][A-Z]/)
signature = line[2]
next unless signature.start_with?('/^extern VALUE ')
globals.append(global_name)
end
File.readlines('tags', chomp: true).each do |line|
line = line.split("\t")
next unless line[3] == 'p'
func_name = line[0]
next unless func_name.start_with?('rb_')
next if func_name.end_with?('_static')
next if IGNORED_FUNCTIONS.include?(func_name)
# Only bind functions whose return type matches one of the return types we have a handler for
ret = line[2]
next unless ret.start_with?('/^') && ret.include?('(')
ret = ret[2..].partition('(')[0].strip
next unless ret.include?(' ') && ret.rpartition(' ')[2].end_with?(func_name)
ret = ret[...-func_name.length].strip
next unless RET_HANDLERS.include?(ret)
# Only bind functions whose arguments all match a return type we have a handler for
args = line[4]
next unless args.start_with?('signature:(') && args.end_with?(')')
args = args[11...-1]
args = args.gsub('VALUE,VALUE', '$').split(',').map { |arg| arg.gsub('$', 'VALUE,VALUE') }.map { |arg| arg == '...' ? '...' : arg.match?(/\(\* \w+\)/) ? arg.gsub(/\(\* \w+\)/, '(*)') : arg.rpartition(' ')[0].strip }
next unless (0...args.length).all? { |i| args[i] == '...' || (ARG_HANDLERS.include?(args[i]) && (ARG_HANDLERS[args[i]][:condition].nil? || ARG_HANDLERS[args[i]][:condition].call(func_name, args, i))) }
coroutine_initializer = ''
destructor = []
transformed_args = Set[]
buffers = []
i = 0
args.each_with_index do |arg, i|
next if arg == '...'
handler = ARG_HANDLERS[arg]
# Generate bindings for converting the arguments
if !handler[:func_ptr_args].nil? || handler[:anyargs]
coroutine_initializer += <<~HEREDOC
f#{i} = bind.sbindgen_create_func_ptr();
if (f#{i} == (wasm_ptr_t)-1) throw SandboxOutOfMemoryException();
HEREDOC
if handler[:anyargs]
coroutine_initializer += <<~HEREDOC
bind.instance->w2c_T0.data[f#{i}] = wasm_rt_funcref_t {
.func_type = wasm2c_#{MODULE_NAME}_get_func_type(a#{args.length - 1} == -1 ? 3 : a#{args.length - 1} == -2 ? 2 : a#{args.length - 1} + 1, 1, #{([:size] * 16).map { |type| FUNC_TYPE_TABLE[type] }.join(', ')}),
.func = (wasm_rt_function_ptr_t)a#{i},
.func_tailcallee = {.fn = NULL},
.module_instance = bind.instance.get(),
};
HEREDOC
else
coroutine_initializer += <<~HEREDOC
bind.instance->w2c_T0.data[f#{i}] = wasm_rt_funcref_t {
.func_type = wasm2c_#{MODULE_NAME}_get_func_type(#{handler[:func_ptr_args].length}, #{handler[:func_ptr_rets].length}#{handler[:func_ptr_args].empty? && handler[:func_ptr_rets].empty? ? '' : ', ' + (handler[:func_ptr_args] + handler[:func_ptr_rets]).map { |type| FUNC_TYPE_TABLE[type] }.join(', ')}),
.func = (wasm_rt_function_ptr_t)a#{i},
.func_tailcallee = {.fn = NULL},
.module_instance = bind.instance.get(),
};
HEREDOC
end
coroutine_initializer += "\n"
transformed_args.add(i)
elsif !handler[:buf_size].nil?
coroutine_initializer += <<~HEREDOC
f#{i} = bind.sbindgen_malloc(#{handler[:buf_size].gsub('PREV_ARG', "a#{i - 1}").gsub('ARG', "a#{i}")});
if (f#{i} == 0) throw SandboxOutOfMemoryException();
HEREDOC
coroutine_initializer += handler[:serialize].gsub('PREV_ARG', "a#{i - 1}").gsub('ARG', "a#{i}").gsub('BUF', "f#{i}")
coroutine_initializer += "\n"
transformed_args.add(i)
buffers.append("f#{i}")
end
i += 1
end
coroutine_vars = []
# If this is a varargs function, manually generate bindings for getting the varargs based on the function name
if !args.empty? && args[-1] == '...'
case func_name
when 'rb_funcall'
coroutine_initializer += <<~HEREDOC
f#{args.length - 1} = bind.sbindgen_malloc(a#{args.length - 2} * sizeof(VALUE));
if (f#{args.length - 1} == 0) throw SandboxOutOfMemoryException();
std::va_list a;
va_start(a, a#{args.length - 2});
for (long i = 0; i < a#{args.length - 2}; ++i) {
((VALUE *)(bind.instance->w2c_memory.data + f#{args.length - 1}))[i] = SERIALIZE_PTR(va_arg(a, VALUE));
}
va_end(a);
HEREDOC
coroutine_initializer += "\n"
buffers.append("f#{args.length - 1}")
when 'rb_rescue2'
coroutine_vars.append('wasm_size_t n')
coroutine_initializer += <<~HEREDOC
std::va_list a, b;
va_start(a, a#{args.length - 2});
va_copy(b, a);
n = 0;
do ++n; while (va_arg(b, VALUE));
va_end(b);
f#{args.length - 1} = bind.sbindgen_malloc(n * sizeof(VALUE));
if (f#{args.length - 1} == 0) {
va_end(a);
throw SandboxOutOfMemoryException();
}
for (wasm_size_t i = 0; i < n; ++i) {
((VALUE *)(bind.instance->w2c_memory.data + f#{args.length - 1}))[i] = SERIALIZE_PTR(va_arg(a, VALUE));
}
HEREDOC
coroutine_initializer += "\n"
buffers.append("f#{args.length - 1}")
else
next
end
end
handler = RET_HANDLERS[ret]
fields = (0...args.length).filter_map do |i|
(args[i] == '...' || transformed_args.include?(i)) && "wasm_ptr_t f#{i}"
end
coroutine_ret = !RET_HANDLERS[ret][:keep] ? VAR_TYPE_TABLE[RET_HANDLERS[ret][:primitive]] : ret;
coroutine_vars.append("#{coroutine_ret} r") if handler[:primitive] != :void
coroutine_args = (0...args.length).map do |i|
args[i] == '...' ? '...'
: !ARG_HANDLERS[args[i]][:formatter].nil? ? ARG_HANDLERS[args[i]][:formatter].call("a#{i}")
: !ARG_HANDLERS[args[i]][:keep] ? "#{VAR_TYPE_TABLE[ARG_HANDLERS[args[i]][:primitive]]} a#{i}"
: "#{args[i]} a#{i}"
end
declaration_args = (0...args.length).map do |i|
args[i] == '...' ? '...'
: !ARG_HANDLERS[args[i]][:formatter].nil? ? ARG_HANDLERS[args[i]][:formatter].call('')
: !ARG_HANDLERS[args[i]][:keep] ? "#{VAR_TYPE_TABLE[ARG_HANDLERS[args[i]][:primitive]]}"
: "#{args[i]}"
end
coroutine_inner = <<~HEREDOC
#{handler[:primitive] == :void ? '' : 'r = '}w2c_#{MODULE_NAME}_#{func_name}(#{(['bind.instance.get()'] + (0...args.length).map { |i| args[i] == '...' || transformed_args.include?(i) ? "f#{i}" : "a#{i}" }).join(', ')});
if (w2c_#{MODULE_NAME}_asyncify_get_state(bind.instance.get()) != 1) break;
yield;
HEREDOC
coroutine_destructor = buffers.empty? ? '' : <<~HEREDOC
#{func_name}::~#{func_name}() {
#{(0...buffers.length).map { |i| " try { if (#{buffers[buffers.length - 1 - i]} != 0) w2c_#{MODULE_NAME}_#{FREE_FUNC}(bind.instance.get(), #{buffers[buffers.length - 1 - i]}); } catch (SandboxTrapException) {}" }.join("\n")}
}
HEREDOC
coroutine_definition = <<~HEREDOC
#{coroutine_ret} #{func_name}::operator()(#{coroutine_args.join(', ')}) {#{coroutine_vars.empty? ? '' : (coroutine_vars.map { |var| "\n #{var} = 0;" }.join + "\n")}
reenter (this) {
#{coroutine_initializer.empty? ? '' : (coroutine_initializer.split("\n").map { |line| " #{line}" }.join("\n") + "\n\n")} for (;;) {
#{coroutine_inner.split("\n").map { |line| " #{line}" }.join("\n")}
}
}#{handler[:primitive] == :void ? '' : "\n\n return r;"}
}#{coroutine_destructor.empty? ? '' : ("\n" + coroutine_destructor)}
HEREDOC
coroutine_declaration = <<~HEREDOC
struct #{func_name} : boost::asio::coroutine {
friend struct bindings;
friend struct bindings::stack_frame<struct #{func_name}>;
#{coroutine_ret} operator()(#{declaration_args.join(', ')});
#{coroutine_destructor.empty? ? '' : "~#{func_name}();\n "}private:
struct bindings &bind;
inline #{func_name}(struct bindings &b) : #{(['bind(b)'] + buffers.map { |buffer| "#{buffer}(0)" }).join(', ')} {}
#{fields.empty? ? '' : fields.map { |field| " #{field};\n" }.join}};
HEREDOC
func_names.append(func_name)
coroutines.append(coroutine_definition)
declarations.append(coroutine_declaration)
end
File.open('mkxp-sandbox-bindgen.h', 'w') do |file|
file.write(HEADER_START)
for global_name in globals
file.write(" inline VALUE #{global_name}() { return this->instance->w2c_#{global_name}; }\n")
end
for func_name in func_names
file.write(" friend struct #{func_name};\n")
end
file.write(" };")
for declaration in declarations
file.write("\n\n" + declaration.split("\n").map { |line| " #{line}" }.join("\n").rstrip)
end
file.write(HEADER_END)
end
File.open('mkxp-sandbox-bindgen.cpp', 'w') do |file|
file.write(PRELUDE)
for coroutine in coroutines
file.write("\n\n")
file.write(coroutine.rstrip + "\n")
end
end