diff --git a/Cargo.toml b/Cargo.toml index 62ed14f4..076a774b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,3 +60,12 @@ which = "4.0.2" [dev-dependencies] trybuild = "1.0.33" + +[[example]] +name = "hello_world" + +[[example]] +name = "shell" + +[[example]] +name = "process" diff --git a/examples/count-hosts.js b/examples/count-hosts.js new file mode 100644 index 00000000..bea6553d --- /dev/null +++ b/examples/count-hosts.js @@ -0,0 +1,42 @@ +// Copyright 2008 the V8 project authors. All rights reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +function Initialize() { } + +function Process(request) { + if (options.verbose) { + log("Processing " + request.host + request.path + + " from " + request.referrer + "@" + request.userAgent); + } + if (!output[request.host]) { + output[request.host] = 1; + } else { + output[request.host]++ + } +} + +Initialize(); diff --git a/examples/hello_world.rs b/examples/hello_world.rs new file mode 100644 index 00000000..57bdbccc --- /dev/null +++ b/examples/hello_world.rs @@ -0,0 +1,65 @@ +use rusty_v8 as v8; + +fn main() { + // Initialize V8. + let platform = v8::new_default_platform().unwrap(); + v8::V8::initialize_platform(platform); + v8::V8::initialize(); + + // Create a new Isolate and make it the current one. + let isolate = &mut v8::Isolate::new(v8::CreateParams::default()); + + // Create a stack-allocated handle scope. + let handle_scope = &mut v8::HandleScope::new(isolate); + + // Create a new context. + let context = v8::Context::new(handle_scope); + + // Enter the context for compiling and running the hello world script. + let scope = &mut v8::ContextScope::new(handle_scope, context); + + // Create a string containing the JavaScript source code. + let code = v8::String::new(scope, "'Hello' + ' World!'").unwrap(); + + // Compile the source code. + let script = v8::Script::compile(scope, code, None).unwrap(); + // Run the script to get the result. + let result = script.run(scope).unwrap(); + + // Convert the result to a string and print it. + let result = result.to_string(scope).unwrap(); + println!("{}", result.to_rust_string_lossy(scope)); + + // Use the JavaScript API to generate a WebAssembly module. + // + // |bytes| contains the binary format for the following module: + // + // (func (export "add") (param i32 i32) (result i32) + // get_local 0 + // get_local 1 + // i32.add) + // + let c_source = r#" + let bytes = new Uint8Array([ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x07, 0x01, + 0x60, 0x02, 0x7f, 0x7f, 0x01, 0x7f, 0x03, 0x02, 0x01, 0x00, 0x07, + 0x07, 0x01, 0x03, 0x61, 0x64, 0x64, 0x00, 0x00, 0x0a, 0x09, 0x01, + 0x07, 0x00, 0x20, 0x00, 0x20, 0x01, 0x6a, 0x0b + ]); + let module = new WebAssembly.Module(bytes); + let instance = new WebAssembly.Instance(module); + instance.exports.add(3, 4); + "#; + // Create a string containing the JavaScript source code. + let source = v8::String::new(scope, c_source).unwrap(); + + // Compile the source code. + let script = v8::Script::compile(scope, source, None).unwrap(); + + // Run the script to get the result. + let result = script.run(scope).unwrap(); + + // Print the result. + let result = result.to_uint32(scope).unwrap(); + println!("3 + 4 = {}", result.value()); +} diff --git a/examples/process.rs b/examples/process.rs new file mode 100644 index 00000000..f0f189dd --- /dev/null +++ b/examples/process.rs @@ -0,0 +1,373 @@ +use rusty_v8 as v8; +use std::collections::HashMap; +use std::convert::TryFrom; + +#[allow(clippy::needless_pass_by_value)] // this function should follow the callback type +fn log_callback( + scope: &mut v8::HandleScope, + args: v8::FunctionCallbackArguments, + mut _retval: v8::ReturnValue, +) { + let message = args + .get(0) + .to_string(scope) + .unwrap() + .to_rust_string_lossy(scope); + + println!("Logged: {}", message); +} + +fn main() { + // Initialize V8. + let platform = v8::new_default_platform().unwrap(); + v8::V8::initialize_platform(platform); + v8::V8::initialize(); + + // Parse options. + let (options, file) = parse_args(); + if file.is_empty() { + panic!("no script was specified"); + } + + let mut isolate = v8::Isolate::new(v8::CreateParams::default()); + let mut scope = v8::HandleScope::new(&mut isolate); + + let source = std::fs::read_to_string(&file) + .unwrap_or_else(|err| panic!("failed to open {}: {}", file, err)); + let source = v8::String::new(&mut scope, &source).unwrap(); + + let mut processor = JsHttpRequestProcessor::new(&mut scope, source, options); + + let requests = vec![ + StringHttpRequest::new("/process.cc", "localhost", "google.com", "firefox"), + StringHttpRequest::new("/", "localhost", "google.net", "firefox"), + StringHttpRequest::new("/", "localhost", "google.org", "safari"), + StringHttpRequest::new("/", "localhost", "yahoo.com", "ie"), + StringHttpRequest::new("/", "localhost", "yahoo.com", "safari"), + StringHttpRequest::new("/", "localhost", "yahoo.com", "firefox"), + ]; + + for req in requests { + processor.process(req); + } + + processor.print_output(); +} + +fn parse_args() -> (HashMap, String) { + use std::env; + let args: Vec = env::args().collect(); + let mut options = HashMap::new(); + let mut file = String::new(); + + for arg in &args { + if let Some(pos) = arg.find('=') { + let (key, value) = arg.split_at(pos); + let value = &value[1..]; + options.insert(key.into(), value.into()); + } else { + file = arg.into(); + } + } + + (options, file) +} + +/// A simplified HTTP request. +trait HttpRequest { + fn path(&self) -> &str; + fn referrer(&self) -> &str; + fn host(&self) -> &str; + fn user_agent(&self) -> &str; +} + +/// A simplified HTTP request. +struct StringHttpRequest { + pub path: String, + pub referrer: String, + pub host: String, + pub user_agent: String, +} + +impl StringHttpRequest { + /// Creates a `StringHttpRequest`. + pub fn new( + path: impl Into, + referrer: impl Into, + host: impl Into, + user_agent: impl Into, + ) -> Self { + Self { + path: path.into(), + referrer: referrer.into(), + host: host.into(), + user_agent: user_agent.into(), + } + } +} + +impl HttpRequest for StringHttpRequest { + fn path(&self) -> &str { + &self.path + } + + fn referrer(&self) -> &str { + &self.referrer + } + + fn host(&self) -> &str { + &self.host + } + + fn user_agent(&self) -> &str { + &self.user_agent + } +} + +/// An http request processor that is scriptable using JavaScript. +struct JsHttpRequestProcessor<'s, 'i> { + context: v8::Local<'s, v8::Context>, + context_scope: v8::ContextScope<'i, v8::HandleScope<'s>>, + process_fn: Option>, + request_template: v8::Global, + _map_template: Option>, +} + +impl<'s, 'i> JsHttpRequestProcessor<'s, 'i> +where + 's: 'i, +{ + /// Creates a scriptable HTTP request processor. + pub fn new( + isolate_scope: &'i mut v8::HandleScope<'s, ()>, + source: v8::Local<'s, v8::String>, + options: HashMap, + ) -> Self { + let global = v8::ObjectTemplate::new(isolate_scope); + global.set( + v8::String::new(isolate_scope, "log").unwrap().into(), + v8::FunctionTemplate::new(isolate_scope, log_callback).into(), + ); + + let context = v8::Context::new_from_template(isolate_scope, global); + let mut context_scope = v8::ContextScope::new(isolate_scope, context); + + let request_template = v8::ObjectTemplate::new(&mut context_scope); + request_template.set_internal_field_count(1); + + // make it global + let request_template = + v8::Global::new(&mut context_scope, request_template); + + let mut self_ = JsHttpRequestProcessor { + context, + context_scope, + process_fn: None, + request_template, + _map_template: None, + }; + + // loads options and output + let options = self_.wrap_map(options); + let options_str = + v8::String::new(&mut self_.context_scope, "options").unwrap(); + self_.context.global(&mut self_.context_scope).set( + &mut self_.context_scope, + options_str.into(), + options.into(), + ); + + let output = v8::Object::new(&mut self_.context_scope); + let output_str = + v8::String::new(&mut self_.context_scope, "output").unwrap(); + self_.context.global(&mut self_.context_scope).set( + &mut self_.context_scope, + output_str.into(), + output.into(), + ); + + // execute script + self_.execute_script(source); + + let process_str = + v8::String::new(&mut self_.context_scope, "Process").unwrap(); + let process_fn = self_ + .context + .global(&mut self_.context_scope) + .get(&mut self_.context_scope, process_str.into()) + .expect("missing function Process"); + + let process_fn = v8::Local::::try_from(process_fn) + .expect("function expected"); + self_.process_fn = Some(process_fn); + + self_ + } + + fn execute_script(&mut self, script: v8::Local<'s, v8::String>) { + let scope = &mut v8::HandleScope::new(&mut self.context_scope); + let try_catch = &mut v8::TryCatch::new(scope); + + let script = v8::Script::compile(try_catch, script, None) + .expect("failed to compile script"); + + if script.run(try_catch).is_none() { + let exception = try_catch.exception().unwrap(); + let exception_string = exception + .to_string(try_catch) + .unwrap() + .to_rust_string_lossy(try_catch); + + panic!("{}", exception_string); + } + } + + /// Processes the given HTTP request. + pub fn process(&mut self, request: R) + where + R: HttpRequest + 'static, + { + let request: Box = Box::new(request); + let request = self.wrap_request(request); + + let scope = &mut v8::HandleScope::new(&mut self.context_scope); + let try_catch = &mut v8::TryCatch::new(scope); + + let process_fn = self.process_fn.as_mut().unwrap(); + let global = self.context.global(try_catch).into(); + + if process_fn + .call(try_catch, global, &[request.into()]) + .is_none() + { + let exception = try_catch.exception().unwrap(); + let exception_string = exception + .to_string(try_catch) + .unwrap() + .to_rust_string_lossy(try_catch); + + panic!("{}", exception_string); + } + } + + /// Utility function that wraps a http request object in a JavaScript object. + fn wrap_request( + &mut self, + request: Box, + ) -> v8::Local<'s, v8::Object> { + // TODO: fix memory leak + + use std::ffi::c_void; + + // Dobule-box to get C-sized reference of Box + let request = Box::new(request); + + // Local scope for temporary handles. + let scope = &mut self.context_scope; + + let request_template = v8::Local::new(scope, &self.request_template); + let result = request_template.new_instance(scope).unwrap(); + + let external = v8::External::new( + scope, + Box::leak(request) as *mut Box as *mut c_void, + ); + + result.set_internal_field(0, external.into()); + + let name = v8::String::new(scope, "path").unwrap().into(); + result.set_accessor(scope, name, Self::request_prop_handler); + let name = v8::String::new(scope, "userAgent").unwrap().into(); + result.set_accessor(scope, name, Self::request_prop_handler); + let name = v8::String::new(scope, "referrer").unwrap().into(); + result.set_accessor(scope, name, Self::request_prop_handler); + let name = v8::String::new(scope, "host").unwrap().into(); + result.set_accessor(scope, name, Self::request_prop_handler); + + result + } + + /// This handles the properties of `HttpRequest` + #[allow(clippy::needless_pass_by_value)] // this function should follow the callback type + fn request_prop_handler( + scope: &mut v8::HandleScope, + key: v8::Local, + args: v8::PropertyCallbackArguments, + mut rv: v8::ReturnValue, + ) { + let this = args.this(); + let external = Self::unwrap_request(scope, this); + + assert!( + !external.is_null(), + "the pointer to Box should not be null" + ); + + let request = unsafe { &mut *external }; + + let key = key.to_string(scope).unwrap().to_rust_string_lossy(scope); + + let value = match &*key { + "path" => request.path(), + "userAgent" => request.user_agent(), + "referrer" => request.referrer(), + "host" => request.host(), + _ => { + return; + } + }; + + rv.set(v8::String::new(scope, value).unwrap().into()); + } + + /// Utility function that extracts the http request object from a wrapper object. + fn unwrap_request<'a>( + scope: &mut v8::HandleScope, + request: v8::Local<'a, v8::Object>, + ) -> *mut Box { + let external = request.get_internal_field(scope, 0).unwrap(); + let external = unsafe { v8::Local::::cast(external) }; + external.value() as *mut Box + } + + fn wrap_map( + &mut self, + options: HashMap, + ) -> v8::Local<'s, v8::Object> { + // TODO: wrap map, not convert into Object + let scope = &mut self.context_scope; + let result = v8::Object::new(scope); + + for (key, value) in options { + let key = v8::String::new(scope, &key).unwrap().into(); + let value = v8::String::new(scope, &value).unwrap().into(); + result.set(scope, key, value); + } + + result + } + + /// Prints the output. + pub fn print_output(&mut self) { + let scope = &mut v8::HandleScope::new(&mut self.context_scope); + let key = v8::String::new(scope, "output").unwrap(); + let output = self + .context + .global(scope) + .get(scope, key.into()) + .unwrap() + .to_object(scope) + .unwrap(); + + let props = output.get_property_names(scope).unwrap(); + for i in 0..props.length() { + let key = props.get_index(scope, i).unwrap(); + let value = output.get(scope, key).unwrap(); + + let key = key.to_string(scope).unwrap().to_rust_string_lossy(scope); + let value = value.to_string(scope).unwrap().to_rust_string_lossy(scope); + + println!("{}: {}", key, value); + } + } +} diff --git a/examples/shell.rs b/examples/shell.rs new file mode 100644 index 00000000..ed8b5436 --- /dev/null +++ b/examples/shell.rs @@ -0,0 +1,222 @@ +use rusty_v8 as v8; + +fn main() { + // Initialize V8. + let platform = v8::new_default_platform().unwrap(); + v8::V8::initialize_platform(platform); + v8::V8::initialize(); + + // Pass command line arguments to V8. + let args: Vec = std::env::args().collect(); + let args = v8::V8::set_flags_from_command_line(args); + + let mut run_shell_flag = args.len() == 1; + let isolate = &mut v8::Isolate::new(v8::CreateParams::default()); + let handle_scope = &mut v8::HandleScope::new(isolate); + + let context = v8::Context::new(handle_scope); + + let context_scope = &mut v8::ContextScope::new(handle_scope, context); + let scope = &mut v8::HandleScope::new(context_scope); + + run_main(scope, &*args, &mut run_shell_flag); + + if run_shell_flag { + run_shell(scope); + } +} + +/// Process remaining command line arguments and execute files +fn run_shell(scope: &mut v8::HandleScope) { + use std::io::{self, Write}; + + println!("V8 version {} [sample shell]", v8::V8::get_version()); + + loop { + print!("> "); + io::stdout().flush().unwrap(); + + let mut buf = String::new(); + match io::stdin().read_line(&mut buf) { + Ok(n) => { + if n == 0 { + println!(); + return; + } + + execute_string(scope, &buf, "(shell)", true, true); + } + Err(error) => println!("error: {}", error), + } + } +} + +/// Process remaining command line arguments and execute files +fn run_main( + scope: &mut v8::HandleScope, + args: &[String], + run_shell: &mut bool, +) { + let mut skip_next = false; + + // Parse command-line arguments. + for (i, arg) in args.iter().enumerate().skip(1) { + if skip_next { + continue; + } + + match &**arg { + "--shell" => { + // Enables the shell. + *run_shell = true; + } + "-f" => { + // Ignore any -f flags for compatibility with the other stand- + // alone JavaScript engines. + } + "-e" => { + // Execute script. + let script: &str = &args[i + 1]; + skip_next = true; + + // TODO: pump event loop (not implemented on rusty_v8?) + // while v8::Platform::pump_message_loop(&platform, isolate) { + // // do nothing + // } + execute_string(scope, script, "unnamed", false, true); + } + arg => { + if arg.starts_with("--") { + eprintln!("Warning: unknown flag {}.\nTry --help for options", arg); + continue; + } + + // Use all other arguments as names of files to load and run. + let script = std::fs::read_to_string(arg).expect("failed to read file"); + execute_string(scope, &script, arg, false, true); + } + } + } +} + +fn execute_string( + scope: &mut v8::HandleScope, + script: &str, + filename: &str, + print_result: bool, + report_exceptions_flag: bool, +) { + let mut scope = v8::TryCatch::new(scope); + + let script = v8::String::new(&mut scope, script).unwrap(); + let origin = v8::ScriptOrigin::new( + v8::String::new(&mut scope, filename).unwrap().into(), + v8::Integer::new(&mut scope, 0), + v8::Integer::new(&mut scope, 0), + v8::Boolean::new(&mut scope, false), + v8::Integer::new(&mut scope, 0), + v8::undefined(&mut scope).into(), + v8::Boolean::new(&mut scope, false), + v8::Boolean::new(&mut scope, false), + v8::Boolean::new(&mut scope, false), + ); + + let script = if let Some(script) = + v8::Script::compile(&mut scope, script, Some(&origin)) + { + script + } else { + assert!(scope.has_caught()); + + if report_exceptions_flag { + report_exceptions(scope); + } + return; + }; + + if let Some(result) = script.run(&mut scope) { + if print_result { + println!( + "{}", + result + .to_string(&mut scope) + .unwrap() + .to_rust_string_lossy(&mut scope) + ); + } + } else { + assert!(scope.has_caught()); + if report_exceptions_flag { + report_exceptions(scope); + } + } +} + +fn report_exceptions(mut try_catch: v8::TryCatch) { + let exception = try_catch.exception().unwrap(); + let exception_string = exception + .to_string(&mut try_catch) + .unwrap() + .to_rust_string_lossy(&mut try_catch); + let message = if let Some(message) = try_catch.message() { + message + } else { + eprintln!("{}", exception_string); + return; + }; + + // Print (filename):(line number): (message). + let filename = message + .get_script_resource_name(&mut try_catch) + .map_or_else( + || "(unknown)".into(), + |s| { + s.to_string(&mut try_catch) + .unwrap() + .to_rust_string_lossy(&mut try_catch) + }, + ); + let line_number = message.get_line_number(&mut try_catch).unwrap_or_default(); + + eprintln!("{}:{}: {}", filename, line_number, exception_string); + + // Print line of source code. + let source_line = message + .get_source_line(&mut try_catch) + .map(|s| { + s.to_string(&mut try_catch) + .unwrap() + .to_rust_string_lossy(&mut try_catch) + }) + .unwrap(); + eprintln!("{}", source_line); + + // Print wavy underline (GetUnderline is deprecated). + let start_column = message.get_start_column(); + let end_column = message.get_end_column(); + + for _ in 0..start_column { + eprint!(" "); + } + + for _ in start_column..end_column { + eprint!("^"); + } + + eprintln!(); + + // Print stack trace + let stack_trace = if let Some(stack_trace) = try_catch.stack_trace() { + stack_trace + } else { + return; + }; + let stack_trace = unsafe { v8::Local::::cast(stack_trace) }; + let stack_trace = stack_trace + .to_string(&mut try_catch) + .map(|s| s.to_rust_string_lossy(&mut try_catch)); + + if let Some(stack_trace) = stack_trace { + eprintln!("{}", stack_trace); + } +}