MediaWiki REL1_34
LuaStandaloneEngine.php
Go to the documentation of this file.
1<?php
2
4use Psr\Log\LoggerInterface;
5use Psr\Log\NullLogger;
6
8 protected static $clockTick;
11
15 protected $interpreter;
16
17 public function load() {
18 parent::load();
19 if ( php_uname( 's' ) === 'Linux' ) {
20 $this->initialStatus = $this->interpreter->getStatus();
21 } else {
22 $this->initialStatus = false;
23 }
24 }
25
27 return [
28 'phpCallsRequireSerialization' => true,
29 ];
30 }
31
32 public function reportLimitData( ParserOutput $output ) {
33 try {
34 $this->load();
35 } catch ( Exception $e ) {
36 return;
37 }
38 if ( $this->initialStatus ) {
39 $status = $this->interpreter->getStatus();
40 $output->setLimitReportData( 'scribunto-limitreport-timeusage',
41 [
42 sprintf( "%.3f", $status['time'] / $this->getClockTick() ),
43 // Strip trailing .0s
44 rtrim( rtrim( sprintf( "%.3f", $this->options['cpuLimit'] ), '0' ), '.' )
45 ]
46 );
47 $output->setLimitReportData( 'scribunto-limitreport-virtmemusage',
48 [
49 $status['vsize'],
50 $this->options['memoryLimit']
51 ]
52 );
53 $output->setLimitReportData( 'scribunto-limitreport-estmemusage',
54 $status['vsize'] - $this->initialStatus['vsize']
55 );
56 }
57 $logs = $this->getLogBuffer();
58 if ( $logs !== '' ) {
59 $output->addModules( 'ext.scribunto.logs' );
60 $output->setLimitReportData( 'scribunto-limitreport-logs', $logs );
61 }
62 }
63
64 public function formatLimitData( $key, &$value, &$report, $isHTML, $localize ) {
65 global $wgLang;
66 $lang = $localize ? $wgLang : Language::factory( 'en' );
67 switch ( $key ) {
68 case 'scribunto-limitreport-logs':
69 if ( $isHTML ) {
70 $report .= $this->formatHtmlLogs( $value, $localize );
71 }
72 return false;
73 case 'scribunto-limitreport-virtmemusage':
74 $value = array_map( [ $lang, 'formatSize' ], $value );
75 break;
76 case 'scribunto-limitreport-estmemusage':
77 // @phan-suppress-next-line PhanTypeMismatchArgument
78 $value = $lang->formatSize( $value );
79 break;
80 }
81 return true;
82 }
83
87 protected function getClockTick() {
88 if ( self::$clockTick === null ) {
89 Wikimedia\suppressWarnings();
90 self::$clockTick = intval( shell_exec( 'getconf CLK_TCK' ) );
91 Wikimedia\restoreWarnings();
92 if ( !self::$clockTick ) {
93 self::$clockTick = 100;
94 }
95 }
96 return self::$clockTick;
97 }
98
102 protected function newInterpreter() {
103 return new Scribunto_LuaStandaloneInterpreter( $this, $this->options + [
104 'logger' => LoggerFactory::getInstance( 'Scribunto' )
105 ] );
106 }
107
108 public function getSoftwareInfo( array &$software ) {
110 if ( $ver !== null ) {
111 if ( substr( $ver, 0, 6 ) === 'LuaJIT' ) {
112 $software['[http://luajit.org/ LuaJIT]'] = str_replace( 'LuaJIT ', '', $ver );
113 } else {
114 $software['[http://www.lua.org/ Lua]'] = str_replace( 'Lua ', '', $ver );
115 }
116 }
117 }
118}
119
121 protected static $nextInterpreterId = 0;
122
126 public $engine;
127
132
136 public $proc;
137
142
146 public $readPipe;
147
152
156 public $id;
157
161 protected $logger;
162
166 protected $callbacks;
167
175 public function __construct( $engine, array $options ) {
176 $this->id = self::$nextInterpreterId++;
177
178 if ( $options['errorFile'] === null ) {
179 $options['errorFile'] = wfGetNull();
180 }
181
182 if ( $options['luaPath'] === null ) {
183 $path = false;
184
185 // Note, if you alter these, also alter getLuaVersion() below
186 if ( PHP_OS == 'Linux' ) {
187 if ( PHP_INT_SIZE == 4 ) {
188 $path = 'lua5_1_5_linux_32_generic/lua';
189 } elseif ( PHP_INT_SIZE == 8 ) {
190 $path = 'lua5_1_5_linux_64_generic/lua';
191 }
192 } elseif ( PHP_OS == 'Windows' || PHP_OS == 'WINNT' || PHP_OS == 'Win32' ) {
193 if ( PHP_INT_SIZE == 4 ) {
194 $path = 'lua5_1_5_Win32_bin/lua5.1.exe';
195 } elseif ( PHP_INT_SIZE == 8 ) {
196 $path = 'lua5_1_5_Win64_bin/lua5.1.exe';
197 }
198 } elseif ( PHP_OS == 'Darwin' ) {
199 $path = 'lua5_1_5_mac_lion_fat_generic/lua';
200 }
201 if ( $path === false ) {
203 'No Lua interpreter was given in the configuration, ' .
204 'and no bundled binary exists for this platform.' );
205 }
206 $options['luaPath'] = __DIR__ . "/binaries/$path";
207
208 if ( !is_executable( $options['luaPath'] ) ) {
209 throw new MWException(
210 sprintf( 'The lua binary (%s) is not executable.', $options['luaPath'] )
211 );
212 }
213 }
214
215 $this->engine = $engine;
216 $this->enableDebug = !empty( $options['debug'] );
217 $this->logger = $options['logger'] ?? new NullLogger();
218
219 $pipes = null;
220 $cmd = wfEscapeShellArg(
221 $options['luaPath'],
222 __DIR__ . '/mw_main.lua',
223 dirname( dirname( __DIR__ ) ),
224 $this->id,
225 PHP_INT_SIZE
226 );
227 if ( php_uname( 's' ) == 'Linux' ) {
228 // Limit memory and CPU
229 $cmd = wfEscapeShellArg(
230 'exec', # proc_open() passes $cmd to 'sh -c' on Linux, so add an 'exec' to bypass it
231 '/bin/sh',
232 __DIR__ . '/lua_ulimit.sh',
233 $options['cpuLimit'], # soft limit (SIGXCPU)
234 $options['cpuLimit'] + 1, # hard limit
235 intval( $options['memoryLimit'] / 1024 ),
236 $cmd );
237 }
238
239 if ( php_uname( 's' ) == 'Windows NT' ) {
240 // Like the passthru() in older versions of PHP,
241 // PHP's invokation of cmd.exe in proc_open() is broken:
242 // http://news.php.net/php.internals/21796
243 // Unlike passthru(), it is not fixed in any PHP version,
244 // so we use the fix similar to one in wfShellExec()
245 $cmd = '"' . $cmd . '"';
246 }
247
248 $this->logger->debug( __METHOD__ . ": creating interpreter: $cmd\n" );
249
250 // Check whether proc_open is available before trying to call it (e.g.
251 // PHP's disable_functions may have removed it)
252 if ( !function_exists( 'proc_open' ) ) {
253 throw $this->engine->newException( 'scribunto-luastandalone-proc-error-proc-open' );
254 }
255
256 // Clear the "last error", so if proc_open fails we can know any
257 // warning was generated by that.
258 Wikimedia\suppressWarnings();
259 trigger_error( '' );
260 Wikimedia\restoreWarnings();
261
262 $this->proc = proc_open(
263 $cmd,
264 [
265 [ 'pipe', 'r' ],
266 [ 'pipe', 'w' ],
267 [ 'file', $options['errorFile'], 'a' ]
268 ],
269 $pipes );
270 if ( !$this->proc ) {
271 $err = error_get_last();
272 if ( !empty( $err['message'] ) ) {
273 throw $this->engine->newException( 'scribunto-luastandalone-proc-error-msg',
274 [ 'args' => [ $err['message'] ] ] );
275 } else {
276 throw $this->engine->newException( 'scribunto-luastandalone-proc-error' );
277 }
278 }
279 $this->writePipe = $pipes[0];
280 $this->readPipe = $pipes[1];
281 }
282
283 public function __destruct() {
284 $this->terminate();
285 }
286
287 public static function getLuaVersion( array $options ) {
288 if ( $options['luaPath'] === null ) {
289 // We know which versions are distributed, no need to run them.
290 if ( PHP_OS == 'Linux' ) {
291 return 'Lua 5.1.5';
292 } elseif ( PHP_OS == 'Windows' || PHP_OS == 'WINNT' || PHP_OS == 'Win32' ) {
293 return 'Lua 5.1.4';
294 } elseif ( PHP_OS == 'Darwin' ) {
295 return 'Lua 5.1.5';
296 } else {
297 return null;
298 }
299 }
300
301 // Ask the interpreter what version it is, using the "-v" option.
302 // The output is expected to be one line, something like these:
303 // Lua 5.1.5 Copyright (C) 1994-2012 Lua.org, PUC-Rio
304 // LuaJIT 2.0.0 -- Copyright (C) 2005-2012 Mike Pall. http://luajit.org/
305 $cmd = wfEscapeShellArg( $options['luaPath'] ) . ' -v';
306 $handle = popen( $cmd, 'r' );
307 if ( $handle ) {
308 $ret = fgets( $handle, 80 );
309 pclose( $handle );
310 if ( $ret && preg_match( '/^Lua(?:JIT)? \S+/', $ret, $m ) ) {
311 return $m[0];
312 }
313 }
314 return null;
315 }
316
317 public function terminate() {
318 if ( $this->proc ) {
319 $this->logger->debug( __METHOD__ . ": terminating\n" );
320 proc_terminate( $this->proc );
321 proc_close( $this->proc );
322 $this->proc = false;
323 }
324 }
325
326 public function quit() {
327 if ( !$this->proc ) {
328 return;
329 }
330 $this->dispatch( [ 'op' => 'quit' ] );
331 proc_close( $this->proc );
332 }
333
334 public function testquit() {
335 if ( !$this->proc ) {
336 return;
337 }
338 $this->dispatch( [ 'op' => 'testquit' ] );
339 proc_close( $this->proc );
340 }
341
347 public function loadString( $text, $chunkName ) {
348 $this->cleanupLuaChunks();
349
350 $result = $this->dispatch( [
351 'op' => 'loadString',
352 'text' => $text,
353 'chunkName' => $chunkName
354 ] );
355 return new Scribunto_LuaStandaloneInterpreterFunction( $this->id, $result[1] );
356 }
357
358 public function callFunction( $func, ...$args ) {
359 if ( !( $func instanceof Scribunto_LuaStandaloneInterpreterFunction ) ) {
360 throw new MWException( __METHOD__ . ': invalid function type' );
361 }
362 if ( $func->interpreterId !== $this->id ) {
363 throw new MWException( __METHOD__ . ': function belongs to a different interpreter' );
364 }
365 $args = func_get_args();
366 unset( $args[0] );
367 // $args is now conveniently a 1-based array, as required by the Lua server
368
369 $this->cleanupLuaChunks();
370
371 $result = $this->dispatch( [
372 'op' => 'call',
373 'id' => $func->id,
374 'nargs' => count( $args ),
375 'args' => $args,
376 ] );
377 // Convert return values to zero-based
378 return array_values( $result );
379 }
380
381 public function wrapPhpFunction( $callable ) {
382 static $uid = 0;
383 $id = "anonymous*" . ++$uid;
384 $this->callbacks[$id] = $callable;
385 $ret = $this->dispatch( [
386 'op' => 'wrapPhpFunction',
387 'id' => $id,
388 ] );
389 return $ret[1];
390 }
391
392 public function cleanupLuaChunks() {
395 $this->dispatch( [
396 'op' => 'cleanupChunks',
398 ] );
399 }
400 }
401
402 public function isLuaFunction( $object ) {
403 return $object instanceof Scribunto_LuaStandaloneInterpreterFunction;
404 }
405
406 public function registerLibrary( $name, array $functions ) {
407 // Make sure all ids are unique, even when libraries share the same name
408 // which is especially relevant for "mw_interface" (T211203).
409 static $uid = 0;
410 $uid++;
411
412 $functionIds = [];
413 foreach ( $functions as $funcName => $callback ) {
414 $id = "$name-$funcName-$uid";
415 $this->callbacks[$id] = $callback;
416 $functionIds[$funcName] = $id;
417 }
418 $this->dispatch( [
419 'op' => 'registerLibrary',
420 'name' => $name,
421 'functions' => $functionIds,
422 ] );
423 }
424
425 public function getStatus() {
426 $result = $this->dispatch( [
427 'op' => 'getStatus',
428 ] );
429 return $result[1];
430 }
431
432 public function pauseUsageTimer() {
433 }
434
435 public function unpauseUsageTimer() {
436 }
437
445 private static function fixNulls( array $array, $count ) {
446 if ( count( $array ) === $count ) {
447 return $array;
448 } else {
449 return array_replace( array_fill( 1, $count, null ), $array );
450 }
451 }
452
453 protected function handleCall( $message ) {
454 $message['args'] = self::fixNulls( $message['args'], $message['nargs'] );
455 try {
456 $result = $this->callback( $message['id'], $message['args'] );
457 } catch ( Scribunto_LuaError $e ) {
458 return [
459 'op' => 'error',
460 'value' => $e->getLuaMessage(),
461 ];
462 }
463
464 // Convert to a 1-based array
465 if ( $result !== null && count( $result ) ) {
466 $result = array_combine( range( 1, count( $result ) ), $result );
467 } else {
468 $result = [];
469 }
470
471 return [
472 'op' => 'return',
473 'nvalues' => count( $result ),
474 'values' => $result
475 ];
476 }
477
478 protected function callback( $id, array $args ) {
479 return ( $this->callbacks[$id] )( ...$args );
480 }
481
482 protected function handleError( $message ) {
483 $opts = [];
484 if ( preg_match( '/^(.*?):(\d+): (.*)$/', $message['value'], $m ) ) {
485 $opts['module'] = $m[1];
486 $opts['line'] = $m[2];
487 $message['value'] = $m[3];
488 }
489 if ( isset( $message['trace'] ) ) {
490 $opts['trace'] = array_values( $message['trace'] );
491 }
492 throw $this->engine->newLuaError( $message['value'], $opts );
493 }
494
495 protected function dispatch( $msgToLua ) {
496 $this->sendMessage( $msgToLua );
497 while ( true ) {
498 $msgFromLua = $this->receiveMessage();
499
500 switch ( $msgFromLua['op'] ) {
501 case 'return':
502 return self::fixNulls( $msgFromLua['values'], $msgFromLua['nvalues'] );
503 case 'call':
504 $msgToLua = $this->handleCall( $msgFromLua );
505 $this->sendMessage( $msgToLua );
506 break;
507 case 'error':
508 $this->handleError( $msgFromLua );
509 return; // not reached
510 default:
511 $this->logger->error( __METHOD__ . ": invalid response op \"{$msgFromLua['op']}\"\n" );
512 throw $this->engine->newException( 'scribunto-luastandalone-decode-error' );
513 }
514 }
515 }
516
517 protected function sendMessage( $msg ) {
518 $this->debug( "TX ==> {$msg['op']}" );
519 $this->checkValid();
520 // Send the message
521 $encMsg = $this->encodeMessage( $msg );
522 if ( !fwrite( $this->writePipe, $encMsg ) ) {
523 // Write error, probably the process has terminated
524 // If it has, handleIOError() will throw. If not, throw an exception ourselves.
525 $this->handleIOError();
526 throw $this->engine->newException( 'scribunto-luastandalone-write-error' );
527 }
528 }
529
530 protected function receiveMessage() {
531 $this->checkValid();
532 // Read the header
533 $header = fread( $this->readPipe, 16 );
534 if ( strlen( $header ) !== 16 ) {
535 $this->handleIOError();
536 throw $this->engine->newException( 'scribunto-luastandalone-read-error' );
537 }
538 $length = $this->decodeHeader( $header );
539
540 // Read the reply body
541 $body = '';
542 $lengthRemaining = $length;
543 while ( $lengthRemaining ) {
544 $buffer = fread( $this->readPipe, $lengthRemaining );
545 if ( $buffer === false || feof( $this->readPipe ) ) {
546 $this->handleIOError();
547 throw $this->engine->newException( 'scribunto-luastandalone-read-error' );
548 }
549 $body .= $buffer;
550 $lengthRemaining -= strlen( $buffer );
551 }
552 $body = strtr( $body, [
553 '\\r' => "\r",
554 '\\n' => "\n",
555 '\\\\' => '\\',
556 ] );
557 $msg = unserialize( $body );
558 $this->debug( "RX <== {$msg['op']}" );
559 return $msg;
560 }
561
562 protected function encodeMessage( $message ) {
563 $serialized = $this->encodeLuaVar( $message );
564 $length = strlen( $serialized );
565 $check = $length * 2 - 1;
566
567 return sprintf( '%08x%08x%s', $length, $check, $serialized );
568 }
569
577 protected function encodeLuaVar( $var, $level = 0 ) {
578 if ( $level > 100 ) {
579 throw new MWException( __METHOD__ . ': recursion depth limit exceeded' );
580 }
581 $type = gettype( $var );
582 switch ( $type ) {
583 case 'boolean':
584 return $var ? 'true' : 'false';
585 case 'integer':
586 return $var;
587 case 'double':
588 if ( !is_finite( $var ) ) {
589 if ( is_nan( $var ) ) {
590 return '(0/0)';
591 }
592 if ( $var === INF ) {
593 return '(1/0)';
594 }
595 if ( $var === -INF ) {
596 return '(-1/0)';
597 }
598 throw new MWException( __METHOD__ . ': cannot convert non-finite number' );
599 }
600 return sprintf( '%.17g', $var );
601 case 'string':
602 return '"' .
603 strtr( $var, [
604 '"' => '\\"',
605 '\\' => '\\\\',
606 "\n" => '\\n',
607 "\r" => '\\r',
608 "\000" => '\\000',
609 ] ) .
610 '"';
611 case 'array':
612 $s = '{';
613 foreach ( $var as $key => $element ) {
614 if ( $s !== '{' ) {
615 $s .= ',';
616 }
617
618 // Lua's number type can't represent most integers beyond 2**53, so stringify such keys
619 if ( is_int( $key ) && ( $key > 9007199254740992 || $key < -9007199254740992 ) ) {
620 $key = sprintf( '%d', $key );
621 }
622
623 $s .= '[' . $this->encodeLuaVar( $key, $level + 1 ) . ']' .
624 '=' . $this->encodeLuaVar( $element, $level + 1 );
625 }
626 $s .= '}';
627 return $s;
628 case 'object':
629 if ( !( $var instanceof Scribunto_LuaStandaloneInterpreterFunction ) ) {
630 throw new MWException( __METHOD__ . ': unable to convert object of type ' .
631 get_class( $var ) );
632 } elseif ( $var->interpreterId !== $this->id ) {
633 throw new MWException(
634 __METHOD__ . ': unable to convert function belonging to a different interpreter'
635 );
636 } else {
637 return 'chunks[' . intval( $var->id ) . ']';
638 }
639 case 'resource':
640 throw new MWException( __METHOD__ . ': unable to convert resource' );
641 case 'NULL':
642 return 'nil';
643 default:
644 throw new MWException( __METHOD__ . ': unable to convert variable of unknown type' );
645 }
646 }
647
648 protected function decodeHeader( $header ) {
649 $length = substr( $header, 0, 8 );
650 $check = substr( $header, 8, 8 );
651 if ( !preg_match( '/^[0-9a-f]+$/', $length ) || !preg_match( '/^[0-9a-f]+$/', $check ) ) {
652 throw $this->engine->newException( 'scribunto-luastandalone-decode-error' );
653 }
654 $length = hexdec( $length );
655 $check = hexdec( $check );
656 if ( $length * 2 - 1 !== $check ) {
657 throw $this->engine->newException( 'scribunto-luastandalone-decode-error' );
658 }
659 return $length;
660 }
661
665 protected function checkValid() {
666 if ( !$this->proc ) {
667 $this->logger->error( __METHOD__ . ": process already terminated\n" );
668 if ( $this->exitError ) {
669 throw $this->exitError;
670 } else {
671 throw $this->engine->newException( 'scribunto-luastandalone-gone' );
672 }
673 }
674 }
675
679 protected function handleIOError() {
680 $this->checkValid();
681
682 // Terminate, fetch the status, then close. proc_close()'s return
683 // value isn't helpful here because there's no way to differentiate a
684 // signal-kill from a normal exit.
685 proc_terminate( $this->proc );
686 while ( true ) {
687 $status = proc_get_status( $this->proc );
688 // XXX: Should proc_get_status docs be changed so that
689 // its documented as possibly returning false?
690 // @phan-suppress-next-line PhanTypeComparisonFromArray
691 if ( $status === false ) {
692 // WTF? Let the caller throw an appropriate error.
693 return;
694 }
695 if ( !$status['running'] ) {
696 break;
697 }
698 usleep( 10000 ); // Give the killed process a chance to be scheduled
699 }
700 proc_close( $this->proc );
701 $this->proc = false;
702
703 // proc_open() sometimes uses a shell, check for shell-style signal reporting.
704 if ( !$status['signaled'] && ( $status['exitcode'] & 0x80 ) === 0x80 ) {
705 $status['signaled'] = true;
706 $status['termsig'] = $status['exitcode'] - 128;
707 }
708
709 if ( $status['signaled'] ) {
710 if ( defined( 'SIGXCPU' ) && $status['termsig'] === SIGXCPU ) {
711 $this->exitError = $this->engine->newException( 'scribunto-common-timeout' );
712 } else {
713 $this->exitError = $this->engine->newException( 'scribunto-luastandalone-signal',
714 [ 'args' => [ $status['termsig'] ] ] );
715 }
716 } else {
717 $this->exitError = $this->engine->newException( 'scribunto-luastandalone-exited',
718 [ 'args' => [ $status['exitcode'] ] ] );
719 }
720 throw $this->exitError;
721 }
722
723 protected function debug( $msg ) {
724 if ( $this->enableDebug ) {
725 $this->logger->debug( "Lua: $msg\n" );
726 }
727 }
728}
729
731 public static $anyChunksDestroyed = [];
732 public static $activeChunkIds = [];
733
738
742 public $id;
743
748 public function __construct( $interpreterId, $id ) {
749 $this->interpreterId = $interpreterId;
750 $this->id = $id;
751 $this->incrementRefCount();
752 }
753
754 public function __clone() {
755 $this->incrementRefCount();
756 }
757
758 public function __wakeup() {
759 $this->incrementRefCount();
760 }
761
762 public function __destruct() {
763 $this->decrementRefCount();
764 }
765
766 private function incrementRefCount() {
767 if ( !isset( self::$activeChunkIds[$this->interpreterId] ) ) {
768 self::$activeChunkIds[$this->interpreterId] = [ $this->id => 1 ];
769 } elseif ( !isset( self::$activeChunkIds[$this->interpreterId][$this->id] ) ) {
770 self::$activeChunkIds[$this->interpreterId][$this->id] = 1;
771 } else {
772 self::$activeChunkIds[$this->interpreterId][$this->id]++;
773 }
774 }
775
776 private function decrementRefCount() {
777 if ( isset( self::$activeChunkIds[$this->interpreterId][$this->id] ) ) {
778 if ( --self::$activeChunkIds[$this->interpreterId][$this->id] <= 0 ) {
779 unset( self::$activeChunkIds[$this->interpreterId][$this->id] );
780 self::$anyChunksDestroyed[$this->interpreterId] = true;
781 }
782 } else {
783 self::$anyChunksDestroyed[$this->interpreterId] = true;
784 }
785 }
786}
unserialize( $serialized)
wfEscapeShellArg(... $args)
Version of escapeshellarg() that works better on Windows.
wfGetNull()
Get a platform-independent path to the null file, e.g.
$wgLang
Definition Setup.php:880
if( $line===false) $args
Definition cdb.php:64
MediaWiki exception.
PSR-3 logger instance factory.
addModules( $modules)
setLimitReportData( $key, $value)
Sets parser limit report data for a key.
An exception class which represents an error in the script.
Definition Common.php:136
formatHtmlLogs( $logs, $localize)
Format the logged data for HTML output.
getLogBuffer()
Get data logged by modules.
getPerformanceCharacteristics()
Get performance characteristics of the Lua engine/interpreter.
getSoftwareInfo(array &$software)
Get software information for Special:Version.
formatLimitData( $key, &$value, &$report, $isHTML, $localize)
Format limit report data.
load()
Initialise the interpreter and the base environment.
reportLimitData(ParserOutput $output)
Add limit report data to a ParserOutput object.
Scribunto_LuaStandaloneInterpreter $interpreter
unpauseUsageTimer()
Unpause CPU usage and limits.
Scribunto_LuaStandaloneEngine $engine
registerLibrary( $name, array $functions)
Register a library of functions.
pauseUsageTimer()
Pause CPU usage and limits.
static fixNulls(array $array, $count)
Fill in missing nulls in a list received from Lua.
wrapPhpFunction( $callable)
Wrap a PHP callable as a Lua function, which can be passed back into Lua.
isLuaFunction( $object)
Test whether an object is a Lua function.
callFunction( $func,... $args)
Call a Lua function.
foreach( $res as $row) $serialized
if(!isset( $args[0])) $lang
$header