1249 lines · 39.2 KB · C
RawDownload
/* * This file is part of mpv. * * mpv is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * mpv is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with mpv.  If not, see <http://www.gnu.org/licenses/>. */ #include <assert.h>#include <string.h>#include <sys/types.h>#include <sys/stat.h>#include <math.h>#include <stdint.h> #include <mujs.h> #include "osdep/io.h"#include "mpv_talloc.h"#include "common/common.h"#include "options/m_property.h"#include "common/msg.h"#include "common/msg_control.h"#include "common/stats.h"#include "options/m_option.h"#include "input/input.h"#include "options/path.h"#include "misc/bstr.h"#include "osdep/timer.h"#include "osdep/threads.h"#include "stream/stream.h"#include "sub/osd.h"#include "core.h"#include "command.h"#include "client.h"#include "libmpv/client.h" // List of builtin modules and their contents as strings.// All these are generated from player/javascript/*.jsstatic const char *const builtin_files[][3] = {    {"@/defaults.js",#   include "player/javascript/defaults.js.inc"    },    {0}}; // Represents a loaded script. Each has its own js state.struct script_ctx {    const char *filename;    const char *path; // NULL if single file    struct mpv_handle *client;    struct MPContext *mpctx;    struct mp_log *log;    char *last_error_str;    size_t js_malloc_size;    struct stats_ctx *stats;}; static struct script_ctx *jctx(js_State *J){    return (struct script_ctx *)js_getcontext(J);} static mpv_handle *jclient(js_State *J){    return jctx(J)->client;} static void pushnode(js_State *J, mpv_node *node);static void makenode(void *ta_ctx, mpv_node *dst, js_State *J, int idx);static int jsL_checkint(js_State *J, int idx);static uint64_t jsL_checkuint64(js_State *J, int idx); /********************************************************************** *  conventions, MuJS notes and vm errors *********************************************************************/// - push_foo functions are called from C and push a value to the vm stack.//// - JavaScript C functions are code which the vm can call as a js function.//   By convention, script_bar and script__baz are js C functions. The former//   is exposed to end users as bar, and _baz is for internal use.//// - js C functions get a fresh vm stack with their arguments, and may//   manipulate their stack as they see fit. On exit, the vm considers the//   top value of their stack as their return value, and GC the rest.//// - js C function's stack[0] is "this", and the rest (1, 2, ...) are the args.//   On entry the stack has at least the number of args defined for the func,//   padded with undefined if called with less, or bigger if called with more.//// - Almost all vm APIs (js_*) may throw an error - a longjmp to the last//   recovery/catch point, which could skip releasing resources. This includes//   js_try itself(!), except at the outer-most [1] js_try which is always//   entering the try part (and the catch part if the try part throws).//   The assumption should be that anything can throw and needs careful setup.//   One such automated setup is the autofree mechanism. Details later.//// - Unless named s_foo, all the functions at this file (inc. init) which//   touch the vm may throw, but either cleanup resources regardless (mostly//   autofree) or leave allocated resources on caller-provided talloc context//   which the caller should release, typically with autofree (e.g. makenode).//// - Functions named s_foo (safe foo) never throw if called at the outer-most//   try-levels, or, inside JS C functions - never throw after allocating.//   If they didn't throw then they return 0 on success, 1 on js-errors.//// [1] In practice the N outer-most (nested) tries are guaranteed to enter the//     try/carch code, where N is the mujs try-stack size (64 with mujs 1.1.3).//     But because we can't track try-level at (called-back) JS C functions,//     it's only guaranteed when we know we're near the outer-most try level. /********************************************************************** *  mpv scripting API error handling *********************************************************************/// - Errors may be thrown on some cases - the reason is at the exception.//// - Some APIs also set last error which can be fetched with mp.last_error(),//   where empty string (false-y) is success, or an error string otherwise.//// - The rest of the APIs are guaranteed to return undefined on error or a//   true-thy value on success and may or may not set last error.//// - push_success, push_failure, push_status and pushed_error set last error. // iserr as true indicates an error, and if so, str may indicate a reason.// Internally ctx->last_error_str is never NULL, and empty indicates success.static void set_last_error(struct script_ctx *ctx, bool iserr, const char *str){    ctx->last_error_str[0] = 0;    if (!iserr)        return;    if (!str || !str[0])        str = "Error";    ctx->last_error_str = talloc_strdup_append(ctx->last_error_str, str);} // For use only by wrappers at defaults.js.// arg: error string. Use empty string to indicate success.static void script__set_last_error(js_State *J){    const char *e = js_tostring(J, 1);    set_last_error(jctx(J), e[0], e);} // mp.last_error() . args: none. return the last error without modifying it.static void script_last_error(js_State *J){    js_pushstring(J, jctx(J)->last_error_str);} // Generic success for APIs which don't return an actual value.static void push_success(js_State *J){    set_last_error(jctx(J), 0, NULL);    js_pushboolean(J, true);} // Doesn't (intentionally) throw. Just sets last_error and pushes undefinedstatic void push_failure(js_State *J, const char *str){    set_last_error(jctx(J), 1, str);    js_pushundefined(J);} // Most of the scripting APIs are either sending some values and getting status// code in return, or requesting some value while providing a default in case an// error happened. These simplify the C code for that and always set last_error. static void push_status(js_State *J, int err){    if (err >= 0) {        push_success(J);    } else {        push_failure(J, mpv_error_string(err));    }}  // If err is success then return 0, else push the item at def and return 1static bool pushed_error(js_State *J, int err, int def){    bool iserr = err < 0;    set_last_error(jctx(J), iserr, iserr ? mpv_error_string(err) : NULL);    if (!iserr)        return false;     js_copy(J, def);    return true;} /********************************************************************** *  Autofree - care-free resource deallocation on vm errors, and otherwise *********************************************************************/// - Autofree (af) functions are called with a talloc context argument which is//   freed after the function exits - either normally or because it threw an//   error, on the latter case it then re-throws the error after the cleanup.////   Autofree js C functions should have an additional void* talloc arg and//   inserted into the vm using af_newcfunction, but otherwise used normally.////  To wrap an autofree function af_TARGET in C://  1. Create a wrapper s_TARGET which does this://      if (js_try(J))//          return 1;//      *af = talloc_new(NULL);//      af_TARGET(J, ..., *af);//      js_endtry(J);//      return 0;//  2. Use s_TARGET like so (frees if allocated, throws if {s,af}_TARGET threw)://       void *af = NULL;//       int r = s_TARGET(J, ..., &af);  // use J, af where the callee expects.//       talloc_free(af);//       if (r)//           js_throw(J);////  The reason that the allocation happens inside try/catch is that js_try//  itself can throw (if it runs out of try-stack) and therefore the code//  inside the try part is not reached - but neither is the catch part(!),//  and instead it throws to the next outer catch - but before we've allocated//  anything, hence no leaks on such case. If js_try did get entered, then the//  allocation happened, and then if af_TARGET threw then s_TARGET will catch//  it (and return 1) and we'll free if afterwards. // add_af_file, add_af_dir, add_af_mpv_alloc take a valid FILE*/DIR*/char* value// respectively, and fclose/closedir/mpv_free it when the parent is freed. static void destruct_af_file(void *p){    fclose(*(FILE**)p);} static void add_af_file(void *parent, FILE *f){    FILE **pf = talloc(parent, FILE*);    *pf = f;    talloc_set_destructor(pf, destruct_af_file);} static void destruct_af_dir(void *p){    closedir(*(DIR**)p);} static void add_af_dir(void *parent, DIR *d){    DIR **pd = talloc(parent, DIR*);    *pd = d;    talloc_set_destructor(pd, destruct_af_dir);} static void destruct_af_mpv_alloc(void *p){    mpv_free(*(char**)p);} static void add_af_mpv_alloc(void *parent, char *ma){    char **p = talloc(parent, char*);    *p = ma;    talloc_set_destructor(p, destruct_af_mpv_alloc);} static void destruct_af_mpv_node(void *p){    mpv_free_node_contents((mpv_node*)p);  // does nothing for MPV_FORMAT_NONE} // returns a new zeroed allocated struct mpv_node, and free it and its content// when the parent is freed.static mpv_node *new_af_mpv_node(void *parent){    mpv_node *p = talloc_zero(parent, mpv_node);  // .format == MPV_FORMAT_NONE    talloc_set_destructor(p, destruct_af_mpv_node);    return p;} // Prototype for autofree functions which can be called from inside the vm.typedef void (*af_CFunction)(js_State*, void*); // safely run autofree js c function directlystatic int s_run_af_jsc(js_State *J, af_CFunction fn, void **af){    if (js_try(J))        return 1;    *af = talloc_new(NULL);    fn(J, *af);    js_endtry(J);    return 0;} // The trampoline function through which all autofree functions are called from// inside the vm. Obtains the target function address and autofree-call it.static void script__autofree(js_State *J){    // The target function is at the "af_" property of this function instance.    js_currentfunction(J);    js_getproperty(J, -1, "af_");    af_CFunction fn = (af_CFunction)js_touserdata(J, -1, "af_fn");    js_pop(J, 2);     void *af = NULL;    int r = s_run_af_jsc(J, fn, &af);    talloc_free(af);    if (r)        js_throw(J);} // Identical to js_newcfunction, but the function is inserted with an autofree// wrapper, and its prototype should have the additional af argument.static void af_newcfunction(js_State *J, af_CFunction fn, const char *name,                            int length){    js_newcfunction(J, script__autofree, name, length);    js_pushnull(J);  // a prototype for the userdata object    js_newuserdata(J, "af_fn", fn, NULL);  // uses a "af_fn" verification tag    js_defproperty(J, -2, "af_", JS_READONLY | JS_DONTENUM | JS_DONTCONF);} /********************************************************************** *  Initialization and file loading *********************************************************************/ static const char *get_builtin_file(const char *name){    for (int n = 0; builtin_files[n][0]; n++) {        if (strcmp(builtin_files[n][0], name) == 0)            return builtin_files[n][1];    }    return NULL;} // Push up to limit bytes of file fname: from builtin_files, else from the OS.static void af_push_file(js_State *J, const char *fname, int limit, void *af){    char *filename = mp_get_user_path(af, jctx(J)->mpctx->global, fname);    MP_VERBOSE(jctx(J), "Reading file '%s'\n", filename);    if (limit < 0)        limit = INT_MAX - 1;     const char *builtin = get_builtin_file(filename);    if (builtin) {        js_pushlstring(J, builtin, MPMIN(limit, strlen(builtin)));        return;    }     // mp.utils.read_file allows partial read up to limit which results in    // error for stream_read_file if the file is larger than limit, so use    // STREAM_ALLOW_PARTIAL_READ to allow reading returning partial results.    // Additionally, disable error logging by stream since the exception    // can be caught and handled by a JS script.    int flags = STREAM_READ_FILE_FLAGS_DEFAULT | STREAM_ALLOW_PARTIAL_READ |                STREAM_SILENT;    bstr data = stream_read_file2(filename, af, flags,                                  jctx(J)->mpctx->global, limit);    if (data.start) {        js_pushlstring(J, data.start, data.len);    } else {        js_error(J, "cannot open file: '%s'", filename);    }} // Safely run af_push_file.static int s_push_file(js_State *J, const char *fname, int limit, void **af){    if (js_try(J))        return 1;    *af = talloc_new(NULL);    af_push_file(J, fname, limit, *af);    js_endtry(J);    return 0;} // Called directly, push up to limit bytes of file fname (from builtin/os).static void push_file_content(js_State *J, const char *fname, int limit){    void *af = NULL;    int r = s_push_file(J, fname, limit, &af);    talloc_free(af);    if (r)        js_throw(J);} // utils.read_file(..). args: fname [,max]. returns [up to max] bytes as string.static void script_read_file(js_State *J){    int limit = js_isundefined(J, 2) ? -1 : jsL_checkint(J, 2);    push_file_content(J, js_tostring(J, 1), limit);} // Runs a file with the caller's this, leaves the stack as is.static void run_file(js_State *J, const char *fname){    MP_VERBOSE(jctx(J), "Loading file %s\n", fname);    push_file_content(J, fname, -1);    js_loadstring(J, fname, js_tostring(J, -1));    js_copy(J, 0);  // use the caller's this    js_call(J, 0);    js_pop(J, 2);  // result, file content} // The spec defines .name and .message for Error objects. Most engines also set// a very convenient .stack = name + message + trace, but MuJS instead sets// .stackTrace = trace only. Normalize by adding such .stack if required.// Run this before anything such that we can get traces on any following errors.static const char *norm_err_proto_js = "\    if (Error().stackTrace && !Error().stack) {\        Object.defineProperty(Error.prototype, 'stack', {\            get: function() {\                return this.name + ': ' + this.message + this.stackTrace;\            }\        });\    }\"; static void add_functions(js_State*, struct script_ctx*); // args: none. called as script, setup and run the main scriptstatic void script__run_script(js_State *J){    js_loadstring(J, "@/norm_err.js", norm_err_proto_js);    js_copy(J, 0);    js_pcall(J, 0);     struct script_ctx *ctx = jctx(J);    add_functions(J, ctx);    run_file(J, "@/defaults.js");    run_file(J, ctx->filename);  // the main file to run     if (!js_hasproperty(J, 0, "mp_event_loop") || !js_iscallable(J, -1))        js_error(J, "no event loop function");    js_copy(J, 0);    js_call(J, 0); // mp_event_loop} // Safely set last error from stack top: stack trace or toString or generic.// May leave items on stack - the caller should detect and pop if it cares.static void s_top_to_last_error(struct script_ctx *ctx, js_State *J){    set_last_error(ctx, 1, "unknown error");    if (js_try(J))        return;    if (js_isobject(J, -1))        js_hasproperty(J, -1, "stack");  // fetches it if exists    set_last_error(ctx, 1, js_tostring(J, -1));    js_endtry(J);} // MuJS can report warnings through this.static void report_handler(js_State *J, const char *msg){    MP_WARN(jctx(J), "[JS] %s\n", msg);} // Safely setup the js vm for calling run_script.static int s_init_js(js_State *J, struct script_ctx *ctx){    if (js_try(J))        return 1;    js_setcontext(J, ctx);    js_setreport(J, report_handler);    js_newcfunction(J, script__run_script, "run_script", 0);    js_pushglobal(J);  // 'this' for script__run_script    js_endtry(J);    return 0;} static void *mp_js_alloc(void *actx, void *ptr, int size_){    if (size_ < 0)        return NULL;     struct script_ctx* ctx = actx;    size_t size = size_, osize = 0;    if (ptr)  // free/realloc        osize = ta_get_size(ptr);     void *ret = talloc_realloc_size(actx, ptr, size);     if (!size || ret) {  // free / successful realloc/malloc        ctx->js_malloc_size = ctx->js_malloc_size - osize + size;        stats_size_value(ctx->stats, "mem", ctx->js_malloc_size);    }    return ret;} /********************************************************************** *  Initialization - booting the script *********************************************************************/// s_load_javascript: (entry point) creates the js vm, runs the script, returns//                    on script exit or uncaught js errors. Never throws.// script__run_script: - loads the built in functions and vars into the vm//                     - runs the default file[s] and the main script file

Lines 1–500 of 1249.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t