Skip to content

Support for hooks (viewing only) #95

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Jan 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
173e4c4
Hook module, basic hook registration and call to Rust callback with data
Oct 7, 2018
261fa88
Allowed FnMut for hooks, hook test file
Oct 7, 2018
f1893db
line_counts test adjustment, support for all fields within lua_Debug
Oct 7, 2018
330c3df
Extra hook tests, to_owned method for Debug, example for set_hook
Oct 8, 2018
d12ae8a
Added execution time limit test + hook removal test
Oct 8, 2018
88b86b5
Tweaked doc for set_hook and other places for hook-related code
Oct 8, 2018
cad12d1
Applied recommendations based on PR, HookOptions -> HookTriggers with…
Oct 8, 2018
d6989b0
Indentation for test code, extra comment for set_hook
Oct 8, 2018
a1f48f5
Made hook unwrapping match rlua errors, trying clearer field names fo…
Oct 9, 2018
198bebc
Whoops, forgot to change the hook triggers field names inside docs
Oct 9, 2018
18af1cc
Finalized choice of field names for HookTriggers and extra doc
Oct 10, 2018
87a3276
Using Rc to store the hook function
Oct 11, 2018
98a181d
Very experimental attempt to block executions within hooks
Oct 11, 2018
c795bd9
Removal of the experimental feature (Lua has it built-in)
Oct 11, 2018
323dcad
Mutable hooks, Lua parameter added, doc and code changes
Oct 11, 2018
53bbbdf
Added hook swapping/removal within hook test
Oct 11, 2018
023e983
Fix for warning in hook so Travis succeeds
Oct 11, 2018
5a2d184
Small change in Lua construction in hook_proc, tests use unwrap()
Oct 11, 2018
7b3d148
Rc cloned before calling its hook, passing borrowed Lua to hook
Oct 12, 2018
9f5f124
make_ephemeral added to make Lua states, fixed build error in hook_proc
Oct 12, 2018
c7122ad
Complete remake of how Debug works (now gets information on demand)
Oct 12, 2018
e158393
Changed public exports, updated doc and tests
Oct 15, 2018
f6717e2
Using derive to implement Default and Clone for HookTriggers
Oct 23, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub type lua_KContext = *mut c_void;
pub type lua_KFunction =
unsafe extern "C" fn(state: *mut lua_State, status: c_int, ctx: lua_KContext) -> c_int;
pub type lua_CFunction = unsafe extern "C" fn(state: *mut lua_State) -> c_int;
pub type lua_Hook = unsafe extern "C" fn(state: *mut lua_State, ar: *mut lua_Debug);

#[repr(C)]
pub struct lua_Debug {
Expand Down Expand Up @@ -78,6 +79,11 @@ pub const LUA_GCSETPAUSE: c_int = 6;
pub const LUA_GCSETSTEPMUL: c_int = 7;
pub const LUA_GCISRUNNING: c_int = 9;

pub const LUA_MASKCALL: c_int = 1;
pub const LUA_MASKRET: c_int = 2;
pub const LUA_MASKLINE: c_int = 4;
pub const LUA_MASKCOUNT: c_int = 8;

extern "C" {
pub fn lua_newstate(alloc: lua_Alloc, ud: *mut c_void) -> *mut lua_State;
pub fn lua_close(state: *mut lua_State);
Expand Down Expand Up @@ -163,6 +169,8 @@ extern "C" {
pub fn lua_gc(state: *mut lua_State, what: c_int, data: c_int) -> c_int;
pub fn lua_getinfo(state: *mut lua_State, what: *const c_char, ar: *mut lua_Debug) -> c_int;

pub fn lua_sethook(state: *mut lua_State, f: Option<lua_Hook>, mask: c_int, count: c_int);

pub fn luaopen_base(state: *mut lua_State) -> c_int;
pub fn luaopen_coroutine(state: *mut lua_State) -> c_int;
pub fn luaopen_table(state: *mut lua_State) -> c_int;
Expand Down
221 changes: 221 additions & 0 deletions src/hook.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
use std::{slice, ptr, str};
use std::marker::PhantomData;
use libc::{self, c_int};
use ffi::{self, lua_State, lua_Debug};
use lua::{Lua, extra_data};
use util::callback_error;

/// Contains information about the running code at the moments specified when setting the hook.
/// All the documentation can be read in the [Lua 5.3 documentaton][lua_doc].
///
/// # Usage
///
/// For optimal performance, the user must call the methods that return the information they need to
/// use. Those methods return a short structure with the data in the fields. That being said,
/// depending on where the hook is called, some methods might return zeroes and empty strings or
/// possibly bad values.
///
/// This structure does not cache the data obtained; you should only call each function once and
/// keep the reference obtained for as long as needed. However, you may not outlive the hook
/// callback; if you need to, please clone the internals that you need and move them outside.
///
/// # Panics
///
/// This structure contains methods that will panic if there is an internal error. If this happens
/// to you, please make an issue to rlua's repository. as this behavior isn't normal.
///
/// [lua_doc]: https://www.lua.org/manual/5.3/manual.html#lua_Debug
#[derive(Clone)]
pub struct Debug<'a> {
ar: *mut lua_Debug,
state: *mut lua_State,
_phantom: PhantomData<&'a ()>
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a private field here to allow rlua to add more fields without breaking users


impl<'a> Debug<'a> {
/// Corresponds to the `n` what mask.
pub fn names(&self) -> Names<'a> {
unsafe {
if ffi::lua_getinfo(self.state, cstr!("n"), self.ar) == 0 {
rlua_panic!("lua_getinfo failed with `n`")
} else {
Names {
name: ptr_to_str((*self.ar).name as *const i8),
name_what: ptr_to_str((*self.ar).namewhat as *const i8)
}
}
}
}

/// Corresponds to the `n` what mask.
pub fn source(&self) -> Source<'a> {
unsafe {
if ffi::lua_getinfo(self.state, cstr!("S"), self.ar) == 0 {
rlua_panic!("lua_getinfo failed with `S`")
} else {
Source {
source: ptr_to_str((*self.ar).source as *const i8),
short_src: ptr_to_str((*self.ar).short_src.as_ptr() as *const i8),
line_defined: (*self.ar).linedefined as i32,
last_line_defined: (*self.ar).lastlinedefined as i32,
what: ptr_to_str((*self.ar).what as *const i8),
}
}
}
}

/// Corresponds to the `l` what mask. Returns the current line.
pub fn curr_line(&self) -> i32 {
unsafe {
if ffi::lua_getinfo(self.state, cstr!("l"), self.ar) == 0 {
rlua_panic!("lua_getinfo failed with `l`")
} else {
(*self.ar).currentline as i32
}
}
}

/// Corresponds to the `t` what mask. Returns true if the hook is in a function tail call, false
/// otherwise.
pub fn is_tail_call(&self) -> bool {
unsafe {
if ffi::lua_getinfo(self.state, cstr!("t"), self.ar) == 0 {
rlua_panic!("lua_getinfo failed with `t`")
} else {
(*self.ar).currentline != 0
}
}
}

/// Corresponds to the `u` what mask.
pub fn stack(&self) -> Stack {
unsafe {
if ffi::lua_getinfo(self.state, cstr!("u"), self.ar) == 0 {
rlua_panic!("lua_getinfo failed with `u`")
} else {
Stack {
num_ups: (*self.ar).nups as i32,
num_params: (*self.ar).nparams as i32,
is_vararg: (*self.ar).isvararg != 0
}
}
}
}
}

#[derive(Clone, Debug)]
pub struct Names<'a> {
pub name: Option<&'a str>,
pub name_what: Option<&'a str>
}

#[derive(Clone, Debug)]
pub struct Source<'a> {
pub source: Option<&'a str>,
pub short_src: Option<&'a str>,
pub line_defined: i32,
pub last_line_defined: i32,
pub what: Option<&'a str>
}

#[derive(Copy, Clone, Debug)]
pub struct Stack {
pub num_ups: i32,
pub num_params: i32,
pub is_vararg: bool
}

/// Indicate in which circumstances the hook should be called by Lua.
///
/// # Usage
///
/// In order to clearly show which fields you are setting to `true` or `Some`, it is highly
/// recommended you use the `Default` trait to fill in any remaining fields. This will also cover
/// you in case new hook features gets added to Lua.
///
/// # Example
///
/// Constructs a `HookTriggers` structure that tells Lua to call the hook after every instruction.
/// Note the use of `Default`.
///
/// ```
/// # use rlua::HookTriggers;
/// # fn main() {
/// let triggers = HookTriggers {
/// every_nth_instruction: Some(1), ..Default::default()
/// };
/// # let _ = triggers;
/// # }
/// ```
#[derive(Clone, Display)]
pub struct HookTriggers {
/// Before a function call.
pub on_calls: bool,
/// When Lua returns from a function.
pub on_returns: bool,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be a good idea to document that this does not include returns from tail-called functions:

For call events, event can be LUA_HOOKCALL, the normal value, or LUA_HOOKTAILCALL, for a tail call; in this case, there will be no corresponding return event.

/// Before executing a new line, or returning from a function call.
pub every_line: bool,
/// After a certain amount of instructions. When set to `Some(count)`, `count` is the number of
/// instructions to execute before calling the hook.
///
/// # Performance
///
/// Setting this to a low value will certainly result in a large overhead due to the crate's
/// safety layer and convenience wrapped over Lua's low-level hooks. `1` is such an example of
/// a high overhead choice.
///
/// Please find a number that is high enough so it's not that bad of an issue, while still
/// having enough precision for your needs. Keep in mind instructions are additions, calls to
/// functions, assignments to variables, etc.; they are very short.
pub every_nth_instruction: Option<u32>,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is super bikeshedding, but would it be possible to discuss these names a bit? I'd prefer to drop the plurality on all of them (which I'm aware makes returns into a keyword), but to avoid that what about something like: on_call, on_return, line or every_line or each_line or something, and after_count? I think after_counts particularly sounds strange.


impl HookTriggers {
/// Compute the mask to pass to `lua_sethook`.
pub(crate) fn mask(&self) -> c_int {
let mut mask: c_int = 0;
if self.on_calls { mask |= ffi::LUA_MASKCALL }
if self.on_returns { mask |= ffi::LUA_MASKRET }
if self.every_line { mask |= ffi::LUA_MASKLINE }
if self.every_nth_instruction.is_some() { mask |= ffi::LUA_MASKCOUNT }
mask
}

/// Returns the `count` parameter to pass to `lua_sethook`, if applicable. Otherwise, zero is
/// returned.
pub(crate) fn count(&self) -> c_int {
self.every_nth_instruction.unwrap_or(0) as c_int
}
}

/// This callback is passed to `lua_sethook` and gets called whenever debug information is received.
pub(crate) unsafe extern "C" fn hook_proc(state: *mut lua_State, ar: *mut lua_Debug) {
callback_error(state, || {
let lua = Lua::make_ephemeral(state);
let debug = Debug {
ar, state, _phantom: PhantomData
};

let cb = (&*extra_data(state)).hook_callback
.as_ref()
.map(|rc| rc.clone())
.expect("rlua internal error: no hooks previously set; this is a bug");
let outcome = match cb.try_borrow_mut() {
Ok(mut b) => (&mut *b)(&lua, &debug),
Err(_) => rlua_panic!("Lua should not allow hooks to be called within another hook;\
please make an issue")
};
outcome
});
}

unsafe fn ptr_to_str<'a>(input: *const i8) -> Option<&'a str> {
if input == ptr::null() || ptr::read(input) == 0 {
return None;
}
let len = libc::strlen(input) as usize;
let input = slice::from_raw_parts(input as *const u8, len);
str::from_utf8(input)
.map(|s| Some(s.trim_right()))
.unwrap_or(None)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like that a method called ptr_to_str is doing trimming, it shouldn't be modifying the strings that Lua returns. Also, I didn't think about the fact that str::from_utf8 is fallible here, so now I'm not sure I like that the Debug structure returns str rather than [u8]. I was recently reminded that we shouldn't make the assumption that Lua strings are utf8 and had gotten into the habit of doing that in too many places.

11 changes: 11 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,20 @@
//! The [`UserData`] trait can be implemented by user-defined types to make them available to Lua.
//! Methods and operators to be used from Lua can be added using the [`UserDataMethods`] API.
//!
//! # Hooks
//!
//! The [`Lua`] struct provides you with a [hook] setter method which you may call to gain
//! information about your script or simply to control it.
//!
//! You are safe to use an `FnMut` function as Lua will never call your hook if you are executing
//! more Lua code within the hook. You have full access to Lua from it.
//!
//! [Lua programming language]: https://www.lua.org/
//! [`Lua`]: struct.Lua.html
//! [executing]: struct.Lua.html#method.exec
//! [evaluating]: struct.Lua.html#method.eval
//! [globals]: struct.Lua.html#method.globals
//! [hook]: struct.Lua.html#method.set_hook
//! [`ToLua`]: trait.ToLua.html
//! [`FromLua`]: trait.FromLua.html
//! [`ToLuaMulti`]: trait.ToLuaMulti.html
Expand Down Expand Up @@ -60,6 +69,7 @@ mod types;
mod userdata;
mod util;
mod value;
pub mod hook;

pub use error::{Error, ExternalError, ExternalResult, Result};
pub use function::Function;
Expand All @@ -72,5 +82,6 @@ pub use thread::{Thread, ThreadStatus};
pub use types::{Integer, LightUserData, Number, RegistryKey};
pub use userdata::{AnyUserData, MetaMethod, UserData, UserDataMethods};
pub use value::{FromLua, FromLuaMulti, MultiValue, Nil, ToLua, ToLuaMulti, Value};
pub use hook::HookTriggers;

pub mod prelude;
Loading