Chapter 5. Detecting Errors

The best way to find a bug is quickly. Sadly, there are many ways to find bugs slowly. Your app could crash long after the bad code executed, or it could crash with no indication as to where in the code the mistake is. This chapter helps you write code that can detect errors as quickly as possible and report a specific code location when things do go wrong.

The C API functions that work with Lua errors are analogous to Lua’s built-in error functions error() and pcall(). So, I’ll begin with a discussion of these two Lua functions and then dive into their C analogues.

Throwing and Catching Errors in Lua

Lua has an error mechanism similar to exceptions. I’ll expand on this analogy, but if exceptions are completely new to you, this chapter gives a self-contained overview of Lua’s error() function.

If you call the error() function in Lua, control flow jumps up the call stack until it finds an error handler. In other languages, this is like a low-level function throwing an exception that is caught higher in the call stack. In Lua, error handlers are set up by wrapping a function call with the pcall() function, which we’ll examine in more detail later in this chapter. If no error handler is present, the error propagates to the top of the call stack universe, escaping into the wild unknown. In that case, Lua invokes a panic, which means that it prints a stack trace and exits the app.

Although the Lua documentation tends to avoid the word “exception,” I’ll use it as a synonym for Lua errors—mainly because the word “error” has such a general meaning, whereas the word exception, in the context of Lua, is more specific and thus clearer.

When Should You Throw an Exception?

Here’s a simplified picture of how code can handle things gone wrong: the code can return values indicating that some kind of bad situation happened; alternatively, the code could throw an exception. It’s subjective whether a given mistake should be handled by a returned error status or by an exception. I like the guideline that exceptions should indicate issues unlikely to occur in clean code; in other words, this guideline suggests using exceptions for errors that were probably caused by a programmer’s mistake rather than by a user’s action. For example, a user typing an incorrect password is a predictable, nonprogrammer error, so it can fit into an exception-free control flow. On the other hand, if some code calls a string function with a numeric value, this is likely to be a mistake made by a programmer. This guideline encourages a low exception rate in well-tested code. But this is by no means a universal guideline.

Don’t Panic!

Here’s an example of a Lua script that throws an uncaught error, resulting in a panic:

-- panic.lua

local function low()
  error('thrown from low')
end

local function middle()
  low()
end

local function high()
  middle()
end

high()
print('Done!')

The top-level code calls high(), which calls middle(), which calls low(), and this last function calls the built-in error() function. No one called pcall(), so there are no error handlers. The resulting panic prints out a stack trace and exits the app. Here’s the output:

$ lua panic.lua
lua: panic.lua:4: thrown from low
stack traceback:
    [C]: in function 'error'
    panic.lua:4: in function 'low'
    panic.lua:8: in function 'middle'
    panic.lua:12: in function 'high'
    panic.lua:15: in main chunk
    [C]: in ?
$

Notice that the last line, print('Done!'), was never called because the app exited before it could run.

Let’s see how that error could have been caught by using the pcall() function. For any standard Lua function call

retval1, retval2 = fn(arg1, arg2, arg3)

there is a corresponding function call that goes through pcall():

did_work, retval1, retval2 = pcall(fn, arg1, arg2, arg3)

Notice that fn() is not called directly on this line; rather, fn() is passed to pcall() as the first argument, and pcall() does the work of actually invoking fn() and passing the given arguments to it. The pcall() function also prepends a new did_work return value to the list of return values from fn(). The value of did_work is true if fn() did not throw an error. However, if fn() did throw an error, that error is caught by pcall(), and no panic occurs. Instead of a panic, pcall() will return a value of false to did_work, and, instead of any return values from fn(), the second return value from pcall() becomes the error object. In theory, the error object is based on whichever value was passed into the error() function. In practice, it’s essentially always a string describing the error.

To summarize, pcall() converts an exception-throwing function call into an exception-free true/false-returning function call.

A slightly modified version of panic.lua can wrap the top-level call to high() using pcall(). Here is the updated version, with changed lines in bold:

-- keep_calm.lua

local function low()
  error('thrown from low')
end

local function middle()
  low()
end

local function high()
  middle()
end

print(pcall(high))
print('Done!')

The output is as follows:

$ lua keep_calm.lua
false keep_calm.lua:4: thrown from low
Done!
$

The false in the output indicates that pcall() caught an error, whereas the rest of that line gives the error string. The error() function prepends the filename and line number to the string “thrown from low.” Notice that Done! was printed, confirming that code was indeed executed after the function call that caused the exception.

Lua Errors in C

Some Lua C API calls can throw Lua errors. For example, suppose that you call lua_newtable(), but the machine is out of memory, and as a result an internal call to malloc() fails. This is unusual but possible—and Lua handles it by throwing an error.

Similar to Lua’s error() and pcall() functions, the C API offers the lua_error() and lua_pcall() functions. Table 5-1 explains how they work.

Table 5-1. The primary C API functions for catching or throwing Lua errors in C
C API function Description Stack use
lua_error(L)
Pop the value from the top of the stack and throw an error with that value. Typically, this value is a string describing the error. [–1, +0]
lua_pcall(L, narg,
          nret, msgh)
Pop the top narg values from the top of the stack, plus the value below that. The last value popped is expected to be a function, and is called with the other narg values as arguments.
If an error is thrown during the call, it is caught and handled based on the value of msgh (described momentarily); in that case, the final error value is pushed onto the top of the stack. If no error is thrown, the first nret return values from the function call are pushed onto the top of the stack.
[–(narg+1),
   +(nret | 1)]

Let’s take a look at an example:

// lua_error.c
#include "lauxlib.h"
#include "lua.h"
#include "lualib.h"

#include <stdio.h>

int fn(lua_State *L) {
  lua_pushstring(L, "thrown from fn()!");
  lua_error(L);
  return 1;
}

int main() {
  lua_State *L = luaL_newstate();
  luaL_openlibs(L);
  lua_register(L, "fn", fn);

  // Call fn().
  lua_getglobal(L, "fn");
  lua_call(L, 0, 0);

  printf("Done!
");

  return 0;
}

This program produces output that looks like the following, with some slight variation across platforms:

$ ./lua_error
PANIC: unprotected error in call to Lua API (thrown from fn()!)
Abort trap: 6
$

This output is based on an uncaught error—and it’s different from the uncaught error output we saw from panic.lua. What gives? It turns out that the built-in reaction to an uncaught Lua error is to print a PANIC message, like in the preceding example, and then to abort the program. The abort action results in the Abort trap: 6 message.

The official Lua interpreter has no special access to Lua’s internals. In other words, it’s built entirely on top of Lua’s C API. As of Lua 5.3.3, the interpreter’s source is 609 lines long and fun to read—you can read it online from the source directory of lua.org. It turns out that all Lua chunks run from the interpreter are invoked by calling a (non-API) function named docall(). Within docall(), the interpreter sets up a function to help handle errors and then uses lua_pcall() to make a protected call to the user’s Lua code. The function that helps handle errors is called a message handler; this is what the argument name msgh is meant to capture in the table that introduced lua_pcall().

How lua_pcall() Handles Errors

If you call lua_pcall() with a msgh value of 0, any caught error value is simply placed on top of the stack and no other action is taken. You can tell whether an error occurred by checking the int return value of lua_pcall(), which will be LUA_OK unless an error was caught. An illustration is worth a thousand words, so let’s consider an example:

int status = lua_pcall(L, narg, nret, 0);  // msgh is 0.
if (status != LUA_OK) {
  // The top value of the stack is an error object.
  // In practice, it's essentially always a string.
  fprintf(stderr, "%s
", lua_tostring(L, -1);
  lua_pop(L, 1);  // Pop the error value.
} else {
  // The call had no errors.
}

(Well, this is embarrassing. According to the word-counting command wc, that example was worth only 56 words, not a thousand. My bad.)

This code pattern enables you to catch errors in C but it’s not perfect because, by the time lua_pcall() returns, the Lua stack has been unwound (that is, it’s as if all the nested function calls between lua_pcall() and error had completed already) so that information about where the error occurred has been lost. You could attempt to work around this by including a stack trace in the error value itself, but this would require a lot of work to be done by every piece of code that throws an error.

Another approach, assisted by lua_pcall(), is to enable a hook function to be called with the value of an error before the stack is unwound. This is a perfect place to add a stack trace to an error without asking any work of the throwing function. In fact, Table 5-2 describes a handy function called luaL_traceback() that can produce a stack trace on your behalf.

Table 5-2. The C API function to generate stack trace information
C API function Description Stack use
luaL_traceback(L, L1,
               <msg>,
               <level>)
Create a string indicating the current stack of function calls in state L1 and push this string onto the top of the stack in state L. In practice, the states L and L1 will often be the same.
The string <msg> can be NULL; if not, it’s prepended to the stack trace.
The value of <level> is an integer indicating how many last-called functions to omit from the stack trace. If luaL_traceback() is called from a message handler, you might want to omit the message handler itself from the stack trace by setting <level> to 1.
[–0, +1]

How can we use luaL_traceback() in a message handler? We can do this by writing up a Lua-callable C function, pushing that function onto the stack, and then calling lua_pcall() with its last argument, msgh, set to the stack index of our Lua-callable function. The message handler is expected to accept one argument (the thrown error value) and to return one value (the replaced error value). The term message handler refers to any Lua-callable function that performs this job. Although this book won’t explain the built-in Lua function xpcall(), it’s good to know that xpcall() exists as an alternative to pcall() that allows you to use message handlers from Lua just as lua_pcall() allows you to use message handlers from C.

Earlier, we made Lua PANIC by throwing an uncaught Lua error from C. I feel a little bad about that because it’s not nice to make someone panic on purpose. In an effort toward redemption, here’s an example in which we responsibly catch an error and use a message handler to provide useful output:

// lua_pcall.c

#include "lauxlib.h"
#include "lua.h"
#include "lualib.h"

#include <stdio.h>

int my_error_handler(lua_State *L) {
  // Push a stack trace string onto the stack.
  // This augmented string will effectively replace the simpler
  // error message that comes directly from the Lua error.
  luaL_traceback(L, L, lua_tostring(L, -1), 1);
  return 1;
}

int fn_that_throws(lua_State *L) {
  lua_pushstring(L, "thrown from fn()!");
  lua_error(L);
  return 1;
}

int middle_fn(lua_State *L) {
  lua_getglobal(L, "fn_that_throws");
  lua_call(L, 0, 0);
  return 0;
}

int main() {

  lua_State *L = luaL_newstate();
  luaL_openlibs(L);

  lua_register(L, "my_error_handler", my_error_handler);
  lua_register(L, "fn_that_throws",   fn_that_throws);
  lua_register(L, "middle_fn",        middle_fn);

  // Call middle_fn().
  lua_pushcfunction(L, my_error_handler);
  lua_getglobal(L, "middle_fn");
  int status = lua_pcall(L, 0, 0, -2);
  if (status != LUA_OK) {
    // Print the error.
    printf("Looks like lua_pcall() caught an error:
%s
",
           lua_tostring(L, -1));
    // Pop the error message from the stack.
    lua_pop(L, 1);
  }

  printf("Done!
");

  return 0;
}

This program makes function calls in this order:

<main> → middle_fn() → fn_that_throws() [ → my_error_handler() ]

The last call, to my_error_handler(), is implicitly performed because that function is on the stack at index –2 when lua_pcall(L, 0, 0, -2) is called (and an error is thrown). If you use lua_pcall() this way, consider printing to stderr instead of stdout in order to more easily separate error output from non-error output.

Here’s the output when we run this program:

$ ./lua_pcall
Looks like lua_pcall() caught an error:
thrown from fn()!
stack traceback:
    [C]: in function 'fn_that_throws'
    [C]: in function 'middle_fn'
Done!

We get a nice stack trace, awareness of an error after the call to lua_pcall(), and the code keeps running smoothly—no panic or abort. What more could you ask for?

Turtles All the Way Down

You’re an astute reader, so I’m sure you’ve been wondering about a particularly nagging issue related to lua_pcall(): what happens if a message handler causes a Lua error while handling a previous Lua error? In that case, the same message handler is called again with the new error, and the return value from that call becomes the replacement error value visible when lua_pcall() completes. These nested calls to the error handler keep happening as long as errors are thrown. But Lua doesn’t have infinite patience; if you keep up this rambunctious behavior long enough, Lua gives up on your error handler and returns the string “error in error handling.”

Getting Strict About Globals and Typos

In Lua, if you refer to a variable that doesn’t exist, nothing explodes. This can be useful for cases in which you want to optionally use values that legitimately might not exist. This same behavior can be bad if you misspell a variable name, because the misspelled variable will always have the value nil—but no error will occur when the misspelled code is run. In other words, Lua makes it more difficult to uncover spelling mistakes because the use of undeclared variables is not an error.

The goal of this section will be to enforce the following two rules:

New global variables cannot be declared within Lua functions
This rule is intended to combat spelling errors in assignments by imposing the simple constraint that globals must be declared outside of a function before they can be assigned a value inside a function.
Global variables must be declared before they’re used
To be clear, declaring a global variable in Lua means nothing more than assigning a value to it for the first time. This second rule helps to detect the use of misspelled global names outside of assignments to them. Variables can still have the value nil, but they must be declared outside of a function before they’re used.

Luckily, there’s a way to enforce these rules. Lua performs lookups of all global variables as if they were keys in a special table; this table is called the environment. In this section, I’ll explain the basics of Lua’s special _ENV variable and then show how you can use it to catch the errors that concern us.

The default Lua environment includes a table with the name _G that contains all global variables as values. Beginning with Lua 5.2, every Lua chunk is also run with a predeclared local variable called _ENV, whose value is a table with the current environment. By default, _ENV is the same as _G, although we’ll soon see some cases in which they might be different. A reference to a variable called name, if it’s not a local variable, is the same as a reference to _ENV.name. Most of the time you can ignore both _ENV and _G and think of Lua’s globals just like any other language’s globals, but in this section, we’ll want to use their magic abilities to enforce the aforementioned rules.

Here’s an example in which the mechanics of _ENV become interesting:

tbl = {}
x = 3
do
  local _ENV = tbl
  x = 4
end
print(x, tbl.x)  -- Prints: 3 4.

The line local _ENV = tbl changes the value of _ENV, but only inside the short do–end block enclosing it. This means that the line x = 4 inside that block has the same effect as_ENV.x = 4. Because _ENV is tbl, this assignment takes the same action as tbl.x = 4. Hence, the value of x outside the block is preserved as 3, whereas the table tbl acquires a new key x with the value 4.

Next, let’s consider the way _ENV interacts with functions like load() and require() that compile Lua code at runtime.

The Environment of Freshly Loaded Chunks

When new chunks of Lua code are loaded, their local _ENV value is set to the global environment, even if _ENV has been changed to something else. The global environment is simply whatever the initial value of _G (which is also the initial value of _ENV) was when the top level of your Lua state began to run.

For example, if you call require() to load a Lua module while in the scope of a customized _ENV table, the module being loaded will not see your custom _ENV table. Instead, it will have its own local _ENV table that is set to the global environment. Here’s an example to illustrate this:

_ENV = {_G = _G, load = load, print = print}
pizza = 'super'
load("pizza = 'awesome'")()
print(_ENV.pizza, _G.pizza)  -- Prints: super awesome.

We save the values of _G, print, and load in the new table assigned to _ENV so we don’t lose access to them after _ENV gets its new value. The built-in load() function compiles (but doesn’t run) the string we give it, and returns a function that, when called, will execute this compiled code. The second pair of parentheses on the load() line call the function returned by load(), causing the compiled code to run. In other words, the line that calls load() is similar to pizza = 'awesome' except that it’s run with its local value of _ENV set to _G. This is why the value of _ENV.pizza is unaffected by the load() line, and the value of _G.pizza is set by that same line.

We can use the _ENV mechanism combined with the __index() and __newindex() metamethods on _ENV to add hook functions that are called every time a new global is declared or used. Because the require() function runs loaded modules in the global environment, these hook functions wouldn’t impose their added restrictions on third-party Lua modules.

Suppose we wrote a function called setup_checked_environment() that installed these hook functions for us. Then, we could turn on strict global name checks by using this code pattern:

_ENV = setup_checked_environment()

-- These modules are allowed to break the rules if they want,
-- which is useful because we wouldn't want to impose our added
-- constraints on third-party modules.
local amodule = require 'amodule'

...
-- The rest of your module code. If your code breaks the global
-- variable rules stated at the start of this section, an error
-- is immediately thrown.

A Version of strict.lua

The end of this section contains the source for a module called strict.lua. Historically, many different approaches to writing this type of module have been proposed, each with various advantages and disadvantages. This particular version of the script works with Lua 5.2 and 5.3 (and quite possibly later versions) because the script uses a replacement table for _ENV. It’s able to work on either a per-file basis or across all files, depending on whether the name-checking metamethods are added to _ENV (to check local files) or _G (to check all files).

Most of this code builds on concepts covered previously. It has the module structure introduced in Chapter 1, defining functions as part of a local table that is returned at the end of the file. It implements the __index metamethod discussed in Chapter 4 as part of class inheritance. Whereas __index runs on lookups to keys that don’t exist in the base table, the __newindex metamethod runs on assignments to keys that don’t exist in the base table.

One part of the code shown here is new, which is the use of the debug module. This module is part of the standard library. The assignment local w = debug.getinfo(2, ’S’).what gives w a string that describes what kind of code called the current function. The first argument, with value 2, specifies level 2 on the call stack. Level 0 would mean debug.getinfo() itself, whereas level 1 would mean the current function (the one that calls debug.getinfo()). The second argument to debug.getinfo(), the string ’S’, asks for information about the source of the calling function. The table returned by debug.getinfo() includes a what key with one of the following string values:

  • Value 'C' means the calling code is C

  • Value 'main' means the calling code is running at the top level of a Lua chunk—that is, outside of any functions

  • Value 'Lua' means the calling code is running from within a Lua function

If the user wants to use globals at all, there must be a valid place to initialize them. The intuitively obvious place is at the top level of a file—so assignments from code with w == 'main' won’t throw errors. Similarly, C code is allowed to assign to global values without an error being thrown. The remaining case, assignments to undeclared globals from within a function, will result in an error—and this is the desired behavior for this case.

Here’s the strict.lua module:

--[[ strict.lua

Tested with Lua 5.2 and 5.3; not designed for 5.1 or earlier.

After the user makes the following statement:

    local _ENV = strict.new_env()

their code will throw an error on either of these conditions:

 * A global variable is created from a nested code block, such
   as within a function, or
 * An undeclared global is referenced.

Alternatively, call:

    strict.add_checks(_G)

to throw errors on all bad global name uses, not just for the
current file.

There are a number of files similar to this one available
online, as well as described in the book Programming in Lua by
Roberto Ierusalimschy. This happens by be a version I like.

--]]

local strict = {}

-- This replaces the __index and __newindex metamethods on the
-- given table; this is designed for use with env set to either
-- _ENV or _G.
function strict.add_checks(env)

  -- Get t's metatable, creating it if needed.
  local mt = getmetatable(env)
  if mt == nil then
    mt = {}
    setmetatable(env, mt)
  end

  -- Set up the declared table to be an upvalue for the __index
  -- and __newindex closures created below.
  local declared = {}

  -- Throw an error for global assignments in non-main Lua code.
  mt.__newindex = function (t, k, v)
    if not declared[k] then
      -- The values (2, 'S') ask for (S for) source info on the
      -- function at level 2 in the stack; the one making the
      -- assignment.
      local w = debug.getinfo(2, 'S').what
      if w ~= 'main' and w ~= 'C' then
        local fmt = 'Assignment to undeclared global "%s"'
        -- The value 2 will blame the error on the code making
        -- the bad assignment; i.e. the caller of __newindex().
        error(fmt:format(k), 2)
      end
      declared[k] = true
    end
    -- Make the actual assignment in the table t.
    rawset(t, k, v)
  end

  -- Throw an error for references to undeclared global names.
  mt.__index = function (t, k)
    if not declared[k] then
      -- The parameter 2 will blame the error on the code making
      -- the bad lookup; i.e. the caller of __index().
      error(('Use of undeclared global "%s"'):format(k), 2)
    end
    -- We won't always get an error; finish the lookup on key k.
    return rawget(t, k)
  end
end

-- This returns a replacement for _ENV that detects errors.
function strict.new_env()

  -- Set up the new environment with the values from _ENV.
  local env = {}
  for key, value in pairs(_ENV) do
    env[key] = value
  end

  -- Add the checks and return the new environment.
  strict.add_checks(env)
  return env
end

return strict

EatyGuy Version 9: Death, Strictly

The previous version of EatyGuy added baddies, but those baddies were unfortunately not very threatening. If you ran into them, nothing would happen. Let’s change that by adding a short function that checks for player–baddy collisions, and ends the game if that happens. I’ll also temporarily introduce a typo and see how strict.lua can alert us to it.

The previous C file, eatyguy8.c, supported exiting only when the player pressed Q or Esc. We can give Lua the ability to end the game by accepting a Lua value named end_msg as a return value from eatyguy.loop(). The game continues as long as this value is nil. As soon as it’s not nil, the game ends, and the string value of end_msg is printed out for the user.

Here is the updated C code, with changes in bold:

// Parts of eatyguy9.c, modified from eatyguy8.c:

void done(const char *msg) {

  // Put the terminal back into a decent state.
  system("stty cooked echo");  // Undo init call to "stty raw".
  system("tput reset");        // Reset colors and clear screen.

  // Print the farewell message if there is one.
  if (msg) printf("%s
", msg);

  exit(0);
}

// ... other code ...

int main() {

  ...

  // Load eatyguy9 and run the init() function.
  luaL_dofile(L, "eatyguy9.lua");
  lua_setglobal(L, "eatyguy");
  lua_settop(L, 0);

  ...

  lua_getglobal(L, "eatyguy");
  while (1) {
    int is_end_of_seq;
    int key = getkey(&is_end_of_seq);

    // Pass NULL to done() to print no ending message.
    if (key == ESC_KEY || key == 'q' || key == 'Q') done(NULL);

    // Call eatyguy.loop(state).
    lua_getfield(L, -1, "loop");
    push_state_table(L, key, is_end_of_seq);
    lua_call(L, 1, 1);

    // Check to see if the game is over.
    if (lua_isstring(L, -1)) {
      const char *msg = lua_tostring(L, -1);
      done(msg);
    }

    // Pop the return value of eatyguy.loop() off the stack.
    lua_pop(L, 1);

    sleephires(0.016);  // Sleep for 16ms.
  }

  return 0;
}

In Lua, we’ll add the use of strict.lua, plus a simple collision-detection function. Here are the updated parts of the Lua code:

-- eatyguy9.lua

local eatyguy = {}


-- Require modules.

local Baddy  = require 'Baddy'
local strict = require 'strict'


-- Enter strict mode.

local _ENV = strict.new_env()


...

-- Globals.

...
local baddies = {}
-- The game ends as soon as end_msg is set to any string; that
-- string will be printed to the user as a farewell message.
local end_msg = nil


-- Internal functions.

local function check_for_death()
  for _, baddy in pairs(baddies) do
    if pair(player.pos) == pair(baddy.pos) then
      end_mgs = 'Game over!'           -- NOTE: Temporary typo!
    end
  end
end

...

local function update(state)

  ...

  -- It's been at least move_delta seconds since the last
  -- time things moved, so let's move them now!
  player:move_if_possible(grid)
  -- Check for baddy collisions both now and after baddies have
  -- moved. With only one check, it may miss the case where both
  -- player and baddy move past each other in one time step.
  check_for_death()
  for _, baddy in pairs(baddies) do
    baddy:move_if_possible(grid)
  end
  check_for_death()
end

...

function eatyguy.loop(state)
  update(state)
  draw(state.clock)
  return end_msg
end

return eatyguy

When this game is run, it begins normally because the typo has not yet been executed. As soon as the misspelled name end_mgs is reached, however, an error is thrown resulting in this message:

PANIC: unprotected error in call to Lua API (eatyguy9.lua:82:   
Assignment to undeclared global "end_mgs")                      
Abort trap: 6 

The message includes a filename and line number, along with the misspelled word! Not too shabby.

To fix the error, simply replace the string end_mgs with the corrected spelling end_msg in eatyguy9.lua. This is on line 82 in the official version of the file in this book’s GitHub repo.

A Brief but Quickly Dismissed Question of Morality

Is the world a better place now that EatyGuy can die? Well, we learned how to write cleaner, better-checked code in the process, so perhaps it was all worth it. In fact, this general trend of making things worse for EatyGuy and educational for us seems to be working in our favor. Why don’t we take things to the next level?

Along those lines, let’s give EatyGuy an awesome new feature: the ability for anyone to write custom scripts that control the baddies. In doing so, life might become stressful and, well, much shorter for EatyGuy—but also more interesting for us because we’ll be giving a lot of power to potentially mischievous users. To protect ourselves from either accidental or malicious abuse of this power, we’ll learn how to impose stringent control over the functionality available to user scripts as well as how to limit their ability to consume system resources. Running scripts with these restrictions is referred to as sandboxing scripts, and this is the topic of Chapter 6.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.75.217