Initial work on wasm scripting backend

This commit is contained in:
Deukhoofd 2022-07-18 10:16:47 +02:00
parent 8eb1159d64
commit 7682704945
Signed by: Deukhoofd
GPG Key ID: F63E044490819F6F
21 changed files with 651 additions and 31 deletions

View File

@ -12,9 +12,10 @@ crate_type = ["rlib"]
path = "src/lib.rs" path = "src/lib.rs"
[features] [features]
c_interface = [] ffi = []
serde = ["dep:serde"] serde = ["dep:serde"]
default = ["serde"] wasm = ["dep:wasmer", "dep:unique-type-id", "dep:unique-type-id-derive"]
default = ["serde", "wasm"]
[profile.dev] [profile.dev]
opt-level = 0 opt-level = 0
@ -50,7 +51,11 @@ rand_pcg = "0.3.1"
hashbrown = "0.12.1" hashbrown = "0.12.1"
indexmap = "1.8.2" indexmap = "1.8.2"
parking_lot = "0.12.1" parking_lot = "0.12.1"
conquer-once = "0.3.2"
serde = { version = "1.0.137", optional = true, features = ["derive"] } serde = { version = "1.0.137", optional = true, features = ["derive"] }
wasmer = { version = "2.3.0", optional = true, default-features = false, features = ["default-cranelift", "universal", "experimental-reference-types-extern-ref"] }
unique-type-id = { version = "1.0.0", optional = true }
unique-type-id-derive = { version = "1.0.0", optional = true }
[dev-dependencies] [dev-dependencies]
csv = "1.1.6" csv = "1.1.6"

View File

@ -1,4 +1,3 @@
use std::ffi::c_void;
use std::ops::Deref; use std::ops::Deref;
use std::sync::Arc; use std::sync::Arc;
@ -15,6 +14,7 @@ use crate::{PkmnResult, StringKey};
/// The dynamic library stores a static data library, as well as holding different libraries and /// The dynamic library stores a static data library, as well as holding different libraries and
/// calculators that might be customized between different generations and implementations. /// calculators that might be customized between different generations and implementations.
#[derive(Debug)] #[derive(Debug)]
#[cfg_attr(feature = "wasm", derive(unique_type_id_derive::UniqueTypeId))]
pub struct DynamicLibrary { pub struct DynamicLibrary {
/// The static data is the immutable storage data for this library. /// The static data is the immutable storage data for this library.
static_data: StaticData, static_data: StaticData,
@ -75,7 +75,7 @@ impl DynamicLibrary {
/// can be created with this combination, returns None. /// can be created with this combination, returns None.
pub fn load_script( pub fn load_script(
&self, &self,
owner: *const c_void, owner: *const u8,
_category: ScriptCategory, _category: ScriptCategory,
_key: &StringKey, _key: &StringKey,
) -> PkmnResult<Option<Arc<dyn Script>>> { ) -> PkmnResult<Option<Arc<dyn Script>>> {

View File

@ -1,4 +1,3 @@
use std::ffi::c_void;
use std::fmt::Debug; use std::fmt::Debug;
use std::sync::Arc; use std::sync::Arc;
@ -14,7 +13,7 @@ pub trait ScriptResolver: Debug {
/// can be created with this combination, returns None. /// can be created with this combination, returns None.
fn load_script( fn load_script(
&self, &self,
owner: *const c_void, owner: *const u8,
category: ScriptCategory, category: ScriptCategory,
script_key: &StringKey, script_key: &StringKey,
) -> PkmnResult<Option<Arc<dyn Script>>>; ) -> PkmnResult<Option<Arc<dyn Script>>>;
@ -29,7 +28,8 @@ pub trait ScriptResolver: Debug {
/// the same name, but a different script. It should be completely valid for a move to have the same /// the same name, but a different script. It should be completely valid for a move to have the same
/// name as an ability, or more commonly: for a script attached to a Pokemon to have the same name as /// name as an ability, or more commonly: for a script attached to a Pokemon to have the same name as
/// a move that placed it there. /// a move that placed it there.
#[derive(Debug, Clone)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[repr(u8)]
pub enum ScriptCategory { pub enum ScriptCategory {
/// A script that belongs to a move. This generally is only the script that is attached to a /// A script that belongs to a move. This generally is only the script that is attached to a
/// [`MoveChoice`](crate::dynamic_data::MoveChoice) and [`ExecutingMove`](crate::dynamic_data::ExecutingMove) /// [`MoveChoice`](crate::dynamic_data::MoveChoice) and [`ExecutingMove`](crate::dynamic_data::ExecutingMove)
@ -58,7 +58,7 @@ pub struct EmptyScriptResolver {}
impl ScriptResolver for EmptyScriptResolver { impl ScriptResolver for EmptyScriptResolver {
fn load_script( fn load_script(
&self, &self,
_owner: *const c_void, _owner: *const u8,
_category: ScriptCategory, _category: ScriptCategory,
_script_key: &StringKey, _script_key: &StringKey,
) -> PkmnResult<Option<Arc<dyn Script>>> { ) -> PkmnResult<Option<Arc<dyn Script>>> {

View File

@ -9,6 +9,8 @@
#![feature(once_cell)] #![feature(once_cell)]
#![feature(const_option)] #![feature(const_option)]
#![feature(is_some_with)] #![feature(is_some_with)]
#![feature(core_ffi_c)]
#![feature(new_uninit)]
//! PkmnLib //! PkmnLib
//! PkmnLib is a full featured implementation of Pokemon. while currently focused on implementing //! PkmnLib is a full featured implementation of Pokemon. while currently focused on implementing
@ -27,6 +29,8 @@ pub mod defines;
/// The dynamic data module holds data that can change during execution, and things that relate to /// The dynamic data module holds data that can change during execution, and things that relate to
/// this. This includes things as Pokemon themselves, battles, etc. /// this. This includes things as Pokemon themselves, battles, etc.
pub mod dynamic_data; pub mod dynamic_data;
/// Script implementations handles the different ways that dynamic scripts get loaded during battle.
pub mod script_implementations;
/// The static data module holds data that can be set once, and then never change. This includes /// The static data module holds data that can be set once, and then never change. This includes
/// things such as data about Pokemon species, data about items, etc. /// things such as data about Pokemon species, data about items, etc.
pub mod static_data; pub mod static_data;

View File

@ -0,0 +1,3 @@
/// The WASM module handles loading dynamic scripts through WebAssembly.
#[cfg(feature = "wasm")]
pub mod wasm;

View File

@ -0,0 +1,103 @@
use core::ffi::c_char;
use std::borrow::Cow;
use std::ffi::CString;
use std::mem::{align_of, size_of};
use wasmer::wasmparser::Data;
use wasmer::{Exports, Function, Store};
use crate::dynamic_data::DynamicLibrary;
use crate::script_implementations::wasm::extern_ref::ExternRef;
use crate::script_implementations::wasm::script_resolver::WebAssemblyEnv;
use crate::static_data::{DataLibrary, MoveData, MoveLibrary, StaticData};
use crate::StringKey;
macro_rules! register_func {
($exports: ident, $store: ident, $func: ident) => {
$exports.insert(stringify!($func), Function::new_native($store, $func));
};
}
macro_rules! register_func_with_env {
($exports: ident, $store: ident, $func: ident, $env: expr) => {
$exports.insert(
stringify!($func),
Function::new_native_with_env($store, $env.clone(), $func),
);
};
}
pub(crate) fn register_webassembly_funcs(exports: &mut Exports, store: &Store, env: WebAssemblyEnv) {
register_func_with_env!(exports, store, _print, env);
register_func_with_env!(exports, store, _error, env);
register_func_with_env!(exports, store, move_library_get_move_by_hash, env);
register_func_with_env!(exports, store, move_data_get_name, env);
register_func_with_env!(exports, store, move_data_get_base_power, env);
register_func_with_env!(exports, store, const_string_get_hash, env);
register_func_with_env!(exports, store, const_string_get_str, env);
register_func_with_env!(exports, store, battle_library_get_data_library, env);
register_func_with_env!(exports, store, data_library_get_move_library, env);
}
fn _print(env: &WebAssemblyEnv, p: u32, len: u32) {
unsafe {
let mem: *mut u8 = env.resolver().memory().data_ptr().offset(p as isize);
let s = String::from_raw_parts(mem, len as usize, len as usize);
println!("{}", s);
}
}
fn _error(env: &WebAssemblyEnv, message: u32, message_len: u32, file: u32, file_len: u32, line: u32, position: u32) {
unsafe {
let mem: *mut u8 = env.resolver().memory().data_ptr().offset(message as isize);
let message = String::from_raw_parts(mem, message_len as usize, message_len as usize);
let mem: *mut u8 = env.resolver().memory().data_ptr().offset(file as isize);
let file = String::from_raw_parts(mem, file_len as usize, file_len as usize);
println!("Error: {} in file {}, line: {}:{}", message, file, line, position);
}
}
fn move_library_get_move_by_hash(env: &WebAssemblyEnv, lib: ExternRef<MoveLibrary>, hash: u32) -> ExternRef<MoveData> {
let lib = lib.value(env);
let m = lib.get_by_hash(hash);
if let Some(v) = m {
ExternRef::new(env, v)
} else {
ExternRef::null()
}
}
fn move_data_get_name(env: &WebAssemblyEnv, move_data: ExternRef<MoveData>) -> ExternRef<StringKey> {
let move_data = move_data.value(env);
ExternRef::new(env, move_data.name())
}
fn move_data_get_base_power(env: &WebAssemblyEnv, move_data: ExternRef<MoveData>) -> u8 {
move_data.value(env).base_power()
}
fn const_string_get_hash(env: &WebAssemblyEnv, string_key: ExternRef<StringKey>) -> u32 {
string_key.value(env).hash()
}
fn const_string_get_str(env: &WebAssemblyEnv, string_key: ExternRef<StringKey>) -> u32 {
let string_key = string_key.value(env).str();
let mut s: CString = CString::new(string_key.as_bytes()).unwrap();
let wasm_string_ptr = env
.resolver()
.allocate_mem(string_key.len() as u32, align_of::<CString>() as u32);
let mut wasm_string = unsafe { CString::from_raw(wasm_string_ptr.0 as *mut c_char) };
s.clone_into(&mut wasm_string);
wasm_string_ptr.1
}
fn battle_library_get_data_library(
env: &WebAssemblyEnv,
dynamic_lib: ExternRef<DynamicLibrary>,
) -> ExternRef<StaticData> {
ExternRef::new(env, dynamic_lib.value(env).static_data())
}
fn data_library_get_move_library(env: &WebAssemblyEnv, data_library: ExternRef<StaticData>) -> ExternRef<MoveLibrary> {
ExternRef::new(env, data_library.value(env).moves())
}

View File

@ -0,0 +1,54 @@
use std::marker::PhantomData;
use unique_type_id::UniqueTypeId;
use wasmer::FromToNativeWasmType;
use crate::script_implementations::wasm::script_resolver::WebAssemblyEnv;
pub(crate) struct ExternRef<T: UniqueTypeId<u64>> {
index: u32,
_phantom: PhantomData<T>,
}
impl<T: UniqueTypeId<u64>> ExternRef<T> {
pub fn new(env: &WebAssemblyEnv, value: &T) -> Self {
Self {
index: env.resolver().get_extern_ref_index(value),
_phantom: Default::default(),
}
}
pub fn from_index(index: u32) -> Self {
Self {
index,
_phantom: Default::default(),
}
}
pub fn null() -> Self {
Self {
index: 0,
_phantom: Default::default(),
}
}
pub fn value<'a, 'b>(&'a self, env: &'b WebAssemblyEnv) -> &'b T {
let ptr = env.resolver().get_extern_ref_value(self.index) as *const T;
unsafe { ptr.as_ref().unwrap() }
}
}
unsafe impl<T: UniqueTypeId<u64>> FromToNativeWasmType for ExternRef<T> {
type Native = i32;
fn from_native(native: Self::Native) -> Self {
Self {
index: native as u32,
_phantom: Default::default(),
}
}
fn to_native(self) -> Self::Native {
self.index as i32
}
}

View File

@ -0,0 +1,70 @@
/// The export registry module deals with registering all functions we require in WebAssembly.
mod export_registry;
pub(crate) mod extern_ref;
/// The script module deals with the actual running of WASM functions.
pub mod script;
/// The script resolver deals with the loading of scripts.
pub mod script_resolver;
/// The WebAssemblyScriptCapabilities define which functions are implemented on a script. This allows
/// us to not call a function if we do not need to.
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
#[allow(missing_docs)]
pub enum WebAssemblyScriptCapabilities {
None = 0,
Initialize = 1,
OnStack,
OnRemove,
OnBeforeTurn,
ChangeAttack,
ModifyNumberOfHits,
PreventAttack,
FailAttack,
StopBeforeAttack,
OnBeforeAttack,
FailIncomingAttack,
IsInvulnerable,
OnAttackMiss,
ChangeAttackType,
ChangeEffectiveness,
BlockCritical,
OnIncomingHit,
OnFaintingOpponent,
PreventStatBoostChange,
ModifyStatBoostChange,
PreventSecondaryEffects,
OnSecondaryEffect,
OnAfterHits,
PreventSelfSwitch,
ModifyEffectChance,
ModifyIncomingEffectChance,
OverrideBasePower,
ChangeDamageStatsUser,
BypassDefensiveStat,
BypassOffensiveStat,
ModifyStatModifier,
ModifyDamageModifier,
OverrideDamage,
OverrideIncomingDamage,
ChangeSpeed,
ChangePriority,
OnFail,
OnOpponentFail,
PreventRunAway,
PreventOpponentRunAway,
PreventOpponentSwitch,
OnEndTurn,
OnDamage,
OnFaint,
OnAfterHeldItemConsume,
PreventIncomingCritical,
ModifyCriticalStage,
OverrideCriticalModifier,
OverrideSTABModifier,
ModifyExperienceGain,
DoesShareExperience,
BlockWeather,
OnSwitchIn,
ModifyOffensiveStatValue,
ModifyDefensiveStatValue,
}

View File

@ -0,0 +1,73 @@
use std::any::Any;
use std::sync::atomic::{AtomicBool, AtomicPtr, AtomicUsize};
use hashbrown::HashSet;
use crate::dynamic_data::Script;
use crate::script_implementations::wasm::script_resolver::WebAssemblyScriptResolver;
use crate::script_implementations::wasm::WebAssemblyScriptCapabilities;
use crate::StringKey;
/// A WebAssemblyScript is there to implement the Script trait within WebAssemblyScript.
pub struct WebAssemblyScript {
/// The unique identifier of the script.
name: StringKey,
/// Returns an atomic bool for internal marking of deletion. This is currently only specifically
/// used for deletion of a script while we are holding a reference to it (i.e. executing a script
/// hook on it).
marked_for_deletion: AtomicBool,
/// A script can be suppressed by other scripts. If a script is suppressed by at least one script
/// we will not execute its methods. This holds the number of suppressions on the script.
suppressed_count: AtomicUsize,
/// The owner of this script (where the script is attached to)
owner_ptr: AtomicPtr<u8>,
/// Pointer inside WebAssembly memory where the data is for this script.
self_ptr: u32,
/// Capabilities define which functions we actually implement.
capabilities: AtomicPtr<HashSet<WebAssemblyScriptCapabilities>>,
/// A reference back to our resolver.
resolver: AtomicPtr<WebAssemblyScriptResolver>,
}
impl WebAssemblyScript {
/// Instantiates a new WebAssemblyScript.
pub fn new(
owner_ptr: *mut u8,
self_ptr: u32,
capabilities: *mut HashSet<WebAssemblyScriptCapabilities>,
resolver: *mut WebAssemblyScriptResolver,
name: StringKey,
) -> Self {
Self {
name,
marked_for_deletion: Default::default(),
suppressed_count: Default::default(),
owner_ptr: AtomicPtr::new(owner_ptr),
self_ptr,
capabilities: AtomicPtr::new(capabilities),
resolver: AtomicPtr::new(resolver),
}
}
}
impl Script for WebAssemblyScript {
fn name(&self) -> &StringKey {
&self.name
}
fn get_marked_for_deletion(&self) -> &AtomicBool {
&self.marked_for_deletion
}
fn get_suppressed_count(&self) -> &AtomicUsize {
&self.suppressed_count
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,253 @@
use std::any::{Any, TypeId};
use std::fmt::{Debug, Formatter};
use std::pin::Pin;
use std::sync::Arc;
use hashbrown::{HashMap, HashSet};
use parking_lot::RwLock;
use unique_type_id::UniqueTypeId;
use wasmer::{
Cranelift, Exports, Extern, Features, Function, ImportObject, Instance, Memory, Module, NativeFunc, Store,
Universal, UniversalEngine, Value, WasmerEnv,
};
use crate::dynamic_data::{ItemScript, Script, ScriptResolver};
use crate::script_implementations::wasm::export_registry::register_webassembly_funcs;
use crate::script_implementations::wasm::extern_ref::ExternRef;
use crate::script_implementations::wasm::script::WebAssemblyScript;
use crate::script_implementations::wasm::WebAssemblyScriptCapabilities;
use crate::static_data::Item;
use crate::{PkmnResult, ScriptCategory, StringKey};
/// A WebAssembly script resolver implements the dynamic scripts functionality with WebAssembly.
pub struct WebAssemblyScriptResolver {
engine: UniversalEngine,
store: Store,
module: Option<Module>,
instance: Option<Instance>,
memory: Option<Memory>,
imports: HashMap<String, Function>,
exports: Exports,
exported_functions: HashMap<StringKey, Function>,
load_script_fn: Option<NativeFunc<(u8, ExternRef<StringKey>), u32>>,
allocate_mem_fn: Option<NativeFunc<(u32, u32), u32>>,
script_capabilities: RwLock<HashMap<ScriptCapabilitiesKey, HashSet<WebAssemblyScriptCapabilities>>>,
extern_ref_pointers: RwLock<Vec<*const u8>>,
extern_ref_pointers_lookup: RwLock<HashMap<*const u8, u32>>,
extern_ref_type_lookup: RwLock<HashMap<*const u8, u64>>,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
struct ScriptCapabilitiesKey {
category: ScriptCategory,
script_key: StringKey,
}
impl WebAssemblyScriptResolver {
/// Instantiates a new WebAssemblyScriptResolver.
pub fn new() -> Self {
let config = Cranelift::default();
let mut features = Features::new();
features.multi_value = true;
features.reference_types = true;
let universal = Universal::new(config).features(features);
let engine = universal.engine();
let store = Store::new(&engine);
Self {
engine,
store,
module: Default::default(),
instance: Default::default(),
memory: Default::default(),
imports: Default::default(),
exports: Default::default(),
exported_functions: Default::default(),
load_script_fn: None,
allocate_mem_fn: None,
script_capabilities: Default::default(),
extern_ref_pointers: Default::default(),
extern_ref_pointers_lookup: Default::default(),
extern_ref_type_lookup: Default::default(),
}
}
/// Load a compiled WASM module.
pub fn load_wasm_from_bytes(&mut self, bytes: &[u8]) {
// FIXME: Error handling
let module = Module::new(&self.store, bytes).unwrap();
self.module = Some(module);
self.finalize();
}
/// Initialise all the data we need.
fn finalize(&mut self) {
let mut imports = ImportObject::new();
let mut exports = Exports::new();
let env = WebAssemblyEnv {
resolver: self as *const WebAssemblyScriptResolver,
};
register_webassembly_funcs(&mut exports, &self.store, env);
imports.register("env", exports);
self.instance = Some(Instance::new(&self.module.as_ref().unwrap(), &imports).unwrap());
let exports = &self.instance.as_ref().unwrap().exports;
for export in exports.iter() {
match export.1 {
Extern::Function(f) => {
self.exported_functions.insert(export.0.as_str().into(), f.clone());
}
Extern::Memory(m) => {
self.memory = Some(m.clone());
}
_ => {}
}
}
if let Some(m) = &self.memory {
m.grow(32).unwrap();
}
if let Some(f) = self.exported_functions.get(&"load_script".into()) {
self.load_script_fn = Some(f.native().unwrap())
}
if let Some(f) = self.exported_functions.get(&"allocate_mem".into()) {
self.allocate_mem_fn = Some(f.native().unwrap())
}
}
/// Gets the internal WASM memory.
pub fn memory(&self) -> &Memory {
self.memory.as_ref().unwrap()
}
/// Get a numeric value from any given value. This is not a true Extern Ref from WASM, as this
/// is not supported by our current WASM platform (Rust). Instead, this is simply a way to not
/// have to send arbitrary pointer values back and forth with WASM. Only values WASM can actually
/// access can be touched through this, and we ensure the value is the correct type. In the future,
/// when extern refs get actually properly implemented at compile time we might want to get rid
/// of this code.
pub fn get_extern_ref_index<T: UniqueTypeId<u64>>(&self, value: &T) -> u32 {
let ptr = value as *const T as *const u8;
if let Some(v) = self.extern_ref_pointers_lookup.read().get(&ptr) {
return *v as u32;
}
let index = {
let mut extern_ref_guard = self.extern_ref_pointers.write();
extern_ref_guard.push(ptr);
extern_ref_guard.len() as u32
};
self.extern_ref_pointers_lookup.write().insert(ptr, index);
self.extern_ref_type_lookup.write().insert(ptr, T::id().0);
index
}
/// Gets a value from the extern ref lookup. This turns an earlier registered index back into
/// its proper value, validates its type, and returns the value.
pub fn get_extern_ref_value<T: UniqueTypeId<u64>>(&self, index: u32) -> &T {
let read_guard = self.extern_ref_pointers.read();
let ptr = read_guard.get(index as usize).unwrap();
let expected_type_id = self.extern_ref_type_lookup.read()[&ptr];
if expected_type_id != T::id().0 {
panic!("Extern ref was accessed with wrong type");
}
unsafe { (*ptr as *const T).as_ref().unwrap() }
}
/// Allocates memory inside the WASM container with a given size and alignment. This memory is
/// owned by WASM, and is how we can pass memory references that the host allocated to WASM.
/// The return is a tuple containing both the actual pointer to the memory (usable by the host),
/// and the WASM offset to the memory (usable by the client).
pub fn allocate_mem(&self, size: u32, align: u32) -> (*const u8, u32) {
let wasm_ptr = self.allocate_mem_fn.as_ref().unwrap().call(size, align).unwrap();
unsafe {
(
self.memory.as_ref().unwrap().data_ptr().offset(wasm_ptr as isize),
wasm_ptr,
)
}
}
}
impl ScriptResolver for WebAssemblyScriptResolver {
fn load_script(
&self,
owner: *const u8,
category: ScriptCategory,
script_key: &StringKey,
) -> PkmnResult<Option<Arc<dyn Script>>> {
let script = self
.load_script_fn
.as_ref()
.unwrap()
.call(
category as u8,
ExternRef::from_index(self.get_extern_ref_index(script_key)),
)
.unwrap();
if script == 0 {
return Ok(None);
}
let key = ScriptCapabilitiesKey {
category,
script_key: script_key.clone(),
};
if !self.script_capabilities.read().contains_key(&key) {
let mut capabilities = HashSet::new();
unsafe {
if let Some(get_cap) = self.exported_functions.get(&"get_script_capabilities".into()) {
let res = get_cap.call(&[Value::I32(script as i32)]).unwrap();
let ptr = (self.memory.as_ref().unwrap().data_ptr() as *const WebAssemblyScriptCapabilities)
.offset(res[0].i32().unwrap() as isize);
let length = res[1].i32().unwrap() as usize;
for i in 0..length {
capabilities.insert(*ptr.offset(i as isize));
}
}
}
self.script_capabilities.write().insert(key.clone(), capabilities);
}
let read_guard = self.script_capabilities.read();
let capabilities = read_guard.get(&key).unwrap();
Ok(Some(Arc::new(WebAssemblyScript::new(
owner as *mut u8,
script,
capabilities as *const HashSet<WebAssemblyScriptCapabilities>
as *mut HashSet<WebAssemblyScriptCapabilities>,
self as *const WebAssemblyScriptResolver as *mut WebAssemblyScriptResolver,
script_key.clone(),
))))
}
fn load_item_script(&self, _key: &Item) -> PkmnResult<Option<Arc<dyn ItemScript>>> {
todo!()
}
}
impl Debug for WebAssemblyScriptResolver {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str("WebAssemblyScriptResolver")
}
}
#[derive(Clone)]
pub(crate) struct WebAssemblyEnv {
pub resolver: *const WebAssemblyScriptResolver,
}
impl WebAssemblyEnv {
pub fn resolver(&self) -> &WebAssemblyScriptResolver {
unsafe { self.resolver.as_ref().unwrap() }
}
}
unsafe impl Sync for WebAssemblyEnv {}
unsafe impl Send for WebAssemblyEnv {}
impl WasmerEnv for WebAssemblyEnv {}

View File

@ -26,6 +26,11 @@ pub trait DataLibrary<'a, T: 'a> {
self.map().get::<StringKey>(key) self.map().get::<StringKey>(key)
} }
/// Gets a value from the library.
fn get_by_hash(&'a self, key: u32) -> Option<&'a T> {
self.map().get::<u32>(&key)
}
/// Gets a mutable value from the library. /// Gets a mutable value from the library.
fn get_mut(&mut self, key: &StringKey) -> Option<&mut T> { fn get_mut(&mut self, key: &StringKey) -> Option<&mut T> {
self.get_modify().get_mut(key) self.get_modify().get_mut(key)

View File

@ -6,6 +6,7 @@ use crate::StringKey;
/// A library to store all data for moves. /// A library to store all data for moves.
#[derive(Debug)] #[derive(Debug)]
#[cfg_attr(feature = "wasm", derive(unique_type_id_derive::UniqueTypeId))]
pub struct MoveLibrary { pub struct MoveLibrary {
/// The underlying map. /// The underlying map.
map: IndexMap<StringKey, MoveData>, map: IndexMap<StringKey, MoveData>,

View File

@ -9,6 +9,7 @@ use crate::static_data::TypeLibrary;
/// The storage for all different libraries. /// The storage for all different libraries.
#[derive(Debug)] #[derive(Debug)]
#[cfg_attr(feature = "wasm", derive(unique_type_id_derive::UniqueTypeId))]
pub struct StaticData { pub struct StaticData {
/// Several misc settings for the library. /// Several misc settings for the library.
settings: LibrarySettings, settings: LibrarySettings,

View File

@ -59,6 +59,7 @@ pub enum MoveTarget {
/// A move is the skill Pokémon primarily use in battle. This is the data related to that. /// A move is the skill Pokémon primarily use in battle. This is the data related to that.
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
#[cfg_attr(feature = "wasm", derive(unique_type_id_derive::UniqueTypeId))]
pub struct MoveData { pub struct MoveData {
/// The name of the move. /// The name of the move.
name: StringKey, name: StringKey,

View File

@ -1,5 +1,3 @@
use std::lazy::SyncLazy;
use hashbrown::{HashMap, HashSet}; use hashbrown::{HashMap, HashSet};
use crate::static_data::Form; use crate::static_data::Form;
@ -28,7 +26,11 @@ pub struct Species {
} }
/// A cached String Key to get the default form. /// A cached String Key to get the default form.
static DEFAULT_KEY: SyncLazy<StringKey> = SyncLazy::new(|| StringKey::new("default")); static DEFAULT_KEY: conquer_once::OnceCell<StringKey> = conquer_once::OnceCell::uninit();
fn get_default_key() -> StringKey {
DEFAULT_KEY.get_or_init(|| StringKey::new("default")).clone()
}
impl Species { impl Species {
/// Creates a new species. /// Creates a new species.
@ -42,7 +44,7 @@ impl Species {
flags: HashSet<StringKey>, flags: HashSet<StringKey>,
) -> Species { ) -> Species {
let mut forms = HashMap::with_capacity(1); let mut forms = HashMap::with_capacity(1);
forms.insert_unique_unchecked(DEFAULT_KEY.clone(), default_form); forms.insert_unique_unchecked(get_default_key(), default_form);
Species { Species {
id, id,
name: name.clone(), name: name.clone(),
@ -96,7 +98,7 @@ impl Species {
/// Gets the form the Pokemon will have by default, if no other form is specified. /// Gets the form the Pokemon will have by default, if no other form is specified.
pub fn get_default_form(&self) -> &Form { pub fn get_default_form(&self) -> &Form {
self.forms.get(&DEFAULT_KEY).unwrap() self.forms.get(&get_default_key()).unwrap()
} }
/// Gets a random gender. /// Gets a random gender.

View File

@ -1,15 +1,17 @@
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::lazy::SyncLazy;
use std::sync::{Arc, Mutex, Weak}; use std::sync::{Arc, Mutex, Weak};
use conquer_once::OnceCell;
use hashbrown::HashMap; use hashbrown::HashMap;
use indexmap::Equivalent;
/// StringKey is an immutable string that is used for indexing of hashmaps or equality a lot. /// StringKey is an immutable string that is used for indexing of hashmaps or equality a lot.
/// By reference counting the string instead of copying, and caching the hash, we can get some /// By reference counting the string instead of copying, and caching the hash, we can get some
/// free speed out of it. Note that StringKeys also compare case insensitive, so that for example /// free speed out of it. Note that StringKeys also compare case insensitive, so that for example
/// `charmander` == `Charmander`. /// `charmander` == `Charmander`.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[cfg_attr(feature = "wasm", derive(unique_type_id_derive::UniqueTypeId))]
pub struct StringKey { pub struct StringKey {
/// The underlying reference counted string. /// The underlying reference counted string.
str: Arc<str>, str: Arc<str>,
@ -19,9 +21,9 @@ pub struct StringKey {
/// A cache of all allocated strings. This allows us to re-use strings that are often used without /// A cache of all allocated strings. This allows us to re-use strings that are often used without
/// allocation. /// allocation.
static STRING_CACHE: SyncLazy<Mutex<HashMap<u32, Weak<str>>>> = SyncLazy::new(|| Mutex::new(HashMap::new())); static STRING_CACHE: OnceCell<Mutex<HashMap<u32, Weak<str>>>> = OnceCell::uninit();
/// An empty StringKey /// An empty StringKey
static EMPTY: SyncLazy<StringKey> = SyncLazy::new(|| StringKey::new("")); static EMPTY: OnceCell<StringKey> = OnceCell::uninit();
impl StringKey { impl StringKey {
/// Calculates the hash of a string key in a const manner. /// Calculates the hash of a string key in a const manner.
@ -49,7 +51,7 @@ impl StringKey {
/// that value. /// that value.
pub fn new(s: &str) -> Self { pub fn new(s: &str) -> Self {
let hash = StringKey::get_hash(s); let hash = StringKey::get_hash(s);
let mut cache = STRING_CACHE.lock().unwrap(); let mut cache = STRING_CACHE.get_or_init(|| Mutex::new(HashMap::new())).lock().unwrap();
let cached_value = cache.get(&hash); let cached_value = cache.get(&hash);
if let Some(cached_value) = cached_value { if let Some(cached_value) = cached_value {
if let Some(cached_value) = cached_value.upgrade() { if let Some(cached_value) = cached_value.upgrade() {
@ -66,7 +68,7 @@ impl StringKey {
/// Gets the empty StringKey. /// Gets the empty StringKey.
pub fn empty() -> Self { pub fn empty() -> Self {
EMPTY.clone() EMPTY.get_or_init(|| StringKey::new("")).clone()
} }
/// Gets the underlying string for the StringKey. /// Gets the underlying string for the StringKey.
@ -100,6 +102,12 @@ impl From<&str> for StringKey {
} }
} }
impl Equivalent<StringKey> for u32 {
fn equivalent(&self, key: &StringKey) -> bool {
*self == key.hash
}
}
impl Display for StringKey { impl Display for StringKey {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(&*self.str) f.write_str(&*self.str)

View File

@ -1,7 +1,7 @@
use std::convert::TryFrom; use std::convert::TryFrom;
use std::fmt::Debug; use std::fmt::Debug;
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::{BufReader, Read};
use hashbrown::HashSet; use hashbrown::HashSet;
use num_traits::PrimInt; use num_traits::PrimInt;
@ -13,6 +13,7 @@ use pkmn_lib::dynamic_data::Gen7BattleStatCalculator;
use pkmn_lib::dynamic_data::Gen7DamageLibrary; use pkmn_lib::dynamic_data::Gen7DamageLibrary;
use pkmn_lib::dynamic_data::Gen7MiscLibrary; use pkmn_lib::dynamic_data::Gen7MiscLibrary;
use pkmn_lib::dynamic_data::{DynamicLibrary, EmptyScriptResolver}; use pkmn_lib::dynamic_data::{DynamicLibrary, EmptyScriptResolver};
use pkmn_lib::script_implementations::wasm::script_resolver::WebAssemblyScriptResolver;
use pkmn_lib::static_data::{ use pkmn_lib::static_data::{
Ability, AbilityLibrary, BattleItemCategory, DataLibrary, EffectParameter, Form, GrowthRateLibrary, Item, Ability, AbilityLibrary, BattleItemCategory, DataLibrary, EffectParameter, Form, GrowthRateLibrary, Item,
ItemLibrary, LearnableMoves, LibrarySettings, LookupGrowthRate, MoveData, MoveLibrary, Nature, NatureLibrary, ItemLibrary, LearnableMoves, LibrarySettings, LookupGrowthRate, MoveData, MoveLibrary, Nature, NatureLibrary,
@ -32,12 +33,15 @@ pub fn load_library() -> DynamicLibrary {
load_abilities(&path, data.abilities_mut()); load_abilities(&path, data.abilities_mut());
load_moves(&path, &mut data); load_moves(&path, &mut data);
load_species(&path, &mut data); load_species(&path, &mut data);
let mut resolver = WebAssemblyScriptResolver::new();
load_wasm(&path, &mut resolver);
let dynamic = DynamicLibrary::new( let dynamic = DynamicLibrary::new(
data, data,
Box::new(Gen7BattleStatCalculator {}), Box::new(Gen7BattleStatCalculator {}),
Box::new(Gen7DamageLibrary::new(false)), Box::new(Gen7DamageLibrary::new(false)),
Box::new(Gen7MiscLibrary::new()), Box::new(Gen7MiscLibrary::new()),
Box::new(EmptyScriptResolver {}), Box::new(resolver),
); );
dynamic dynamic
} }
@ -271,6 +275,14 @@ pub fn load_species(path: &String, library: &mut StaticData) {
} }
} }
fn load_wasm(path: &String, library: &mut WebAssemblyScriptResolver) {
let mut file = File::open(path.to_string() + "gen7_scripts_rs.wasm").unwrap();
let mut reader = BufReader::new(file);
let mut buffer = Vec::new();
reader.read_to_end(&mut buffer);
library.load_wasm_from_bytes(&buffer);
}
fn parse_form(name: StringKey, value: &Value, library: &mut StaticData) -> Form { fn parse_form(name: StringKey, value: &Value, library: &mut StaticData) -> Form {
let mut abilities = Vec::new(); let mut abilities = Vec::new();
for a in value["abilities"].as_array().unwrap() { for a in value["abilities"].as_array().unwrap() {

BIN
tests/data/gen7_scripts_rs.wasm Executable file

Binary file not shown.

View File

@ -4,27 +4,35 @@
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
use std::lazy::SyncLazy;
use std::path::Path; use std::path::Path;
use pkmn_lib::dynamic_data::DynamicLibrary; use conquer_once::OnceCell;
use pkmn_lib::dynamic_data::{DynamicLibrary, ScriptCategory};
use crate::common::{library_loader, TestCase}; use crate::common::{library_loader, TestCase};
pub mod common; pub mod common;
static LIBRARY: SyncLazy<DynamicLibrary> = SyncLazy::new(|| { static LIBRARY: OnceCell<DynamicLibrary> = OnceCell::uninit();
fn get_library<'a>() -> &'a DynamicLibrary {
LIBRARY.get_or_init(|| {
let start_time = chrono::Utc::now(); let start_time = chrono::Utc::now();
let lib = library_loader::load_library(); let lib = library_loader::load_library();
let end_time = chrono::Utc::now(); let end_time = chrono::Utc::now();
println!("Built library in {} ms", (end_time - start_time).num_milliseconds()); println!("Built library in {} ms", (end_time - start_time).num_milliseconds());
lib lib
}); })
}
#[test] #[test]
#[cfg_attr(miri, ignore)] #[cfg_attr(miri, ignore)]
fn validate_library_load() { fn validate_library_load() {
SyncLazy::force(&LIBRARY); let start_time = chrono::Utc::now();
let lib = library_loader::load_library();
let end_time = chrono::Utc::now();
println!("Built library in {} ms", (end_time - start_time).num_milliseconds());
} }
#[datatest::files("tests/test_cases", { #[datatest::files("tests/test_cases", {
@ -37,5 +45,16 @@ fn integration_tests(input: &Path) {
file.read_to_string(&mut str).unwrap(); file.read_to_string(&mut str).unwrap();
let test_case = serde_yaml::from_str::<TestCase>(&*str).unwrap(); let test_case = serde_yaml::from_str::<TestCase>(&*str).unwrap();
println!("\tRunning integration test {}", test_case.name); println!("\tRunning integration test {}", test_case.name);
test_case.run_test(&LIBRARY); test_case.run_test(get_library());
}
#[test]
#[cfg_attr(miri, ignore)]
fn validate_script() {
let lib = library_loader::load_library();
let script = lib
.load_script(0 as *const u8, ScriptCategory::Move, &"test".into())
.unwrap()
.unwrap();
script.on_initialize(&[]);
} }

5
types.toml Normal file
View File

@ -0,0 +1,5 @@
MoveLibrary = 0
MoveData = 1
StringKey = 2
DynamicLibrary = 3
StaticData = 4