From 772b874defe2e7ef523c3c6cb30a7b5e7f79fbbc Mon Sep 17 00:00:00 2001 From: Michael James Gratton Date: Sun, 1 Jan 2017 15:15:26 +1100 Subject: [PATCH] Clean up JavaScriptCore VAPI, client and engine code. * bindings/vapi/javascriptcore-4.0.vapi: Make JS objects match their JSC definitions: move JSValueFoo methods to JS.Value, etc. Update call sites. * src/client/util/util-webkit.vala: Move WebKit-specific common methods from ClientWebView here. Update call sites. * src/engine/util/util-js.vala: Move JSC-specific common methods from ClientWebView and ComposerPageStateTest here. Update call sites. * src/client/web-process/web-process-extension.vala: Check for and handle exceptions when calling JS code. * src/CMakeLists.txt: Add new source files, make WebKit VAPI generation and engine compilation depend on JSC. --- bindings/vapi/javascriptcore-4.0.vapi | 134 ++++++++++-------- src/CMakeLists.txt | 5 +- src/client/components/client-web-view.vala | 47 +----- src/client/composer/composer-web-view.vala | 4 +- .../conversation-web-view.vala | 4 +- src/client/util/util-webkit.vala | 87 ++++++++++++ .../web-process/web-process-extension.vala | 60 ++++++-- src/engine/util/util-js.vala | 79 +++++++++++ test/js/composer-page-state-test.vala | 70 ++------- 9 files changed, 311 insertions(+), 179 deletions(-) create mode 100644 src/client/util/util-webkit.vala create mode 100644 src/engine/util/util-js.vala diff --git a/bindings/vapi/javascriptcore-4.0.vapi b/bindings/vapi/javascriptcore-4.0.vapi index 7601d1cd..c47d845f 100644 --- a/bindings/vapi/javascriptcore-4.0.vapi +++ b/bindings/vapi/javascriptcore-4.0.vapi @@ -1,4 +1,9 @@ -/* javascriptcore-4.0.vapi. */ +/* + * Copyright 2017 Michael Gratton + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ [CCode (cprefix = "JS", gir_namespace = "JavaScriptCore", gir_version = "4.0", lower_case_cprefix = "JS_", cheader_filename = "JavaScriptCore/JavaScript.h")] namespace JS { @@ -7,27 +12,6 @@ namespace JS { [SimpleType] public struct Context { - [CCode (cname = "JSValueIsBoolean")] - public bool is_boolean(JS.Value value); - - [CCode (cname = "JSValueIsNumber")] - public bool is_number(JS.Value value); - - [CCode (cname = "JSValueIsObject")] - public bool is_object(JS.Value value); - - [CCode (cname = "JSValueToBoolean")] - public bool to_boolean(JS.Value value); - - [CCode (cname = "JSValueToNumber")] - public double to_number(JS.Value value, out JS.Value exception); - - [CCode (cname = "JSValueToObject")] - public Object to_object(JS.Value value, out JS.Value exception); - - [CCode (cname = "JSValueToStringCopy")] - public String to_string_copy(JS.Value value, out JS.Value exception); - [CCode (cname = "JSEvaluateScript")] public Value evaluate_script(String script, Object? thisObject, @@ -35,21 +19,11 @@ namespace JS { int startingLineNumber, out Value? exception); - [CCode (cname = "JSObjectMakeFunction")] - public Object make_function(String? name, - [CCode (array_length_pos=1.5)] - String[]? parameterNames, - String body, - String? sourceURL, - int startingLineNumber, - out Value? exception); - - [CCode (cname = "JSObjectCallAsFunction")] - public Value call_as_function(Object object, - Object? thisObject, - [CCode (array_length_pos=2.5)] - Value[]? arguments, - out Value? exception); + [CCode (cname = "JSCheckScriptSyntax")] + public Value check_script_syntax(String script, + String? sourceURL, + int startingLineNumber, + out Value? exception); } @@ -61,10 +35,48 @@ namespace JS { public bool release(); } + [CCode (cname = "JSType", has_type_id = false)] + public enum Type { + + [CCode (cname = "kJSTypeUndefined")] + UNDEFINED, + + [CCode (cname = "kJSTypeNull")] + NULL, + + [CCode (cname = "kJSTypeBoolean")] + BOOLEAN, + + [CCode (cname = "kJSTypeNumber")] + NUMBER, + + [CCode (cname = "kJSTypeString")] + STRING, + + [CCode (cname = "kJSTypeObject")] + OBJECT + } + [CCode (cname = "JSObjectRef")] [SimpleType] public struct Object { + [CCode (cname = "JSObjectMakeFunction")] + public Object.make_function(String? name, + [CCode (array_length_pos=1.5)] + String[]? parameterNames, + String body, + String? sourceURL, + int startingLineNumber, + out Value? exception); + + [CCode (cname = "JSObjectCallAsFunction", instance_pos = 1.1)] + public Value call_as_function(Context ctx, + Object? thisObject, + [CCode (array_length_pos=2.5)] + Value[]? arguments, + out Value? exception); + [CCode (cname = "JSObjectHasProperty", instance_pos = 1.1)] public bool has_property(Context ctx, String property_name); @@ -80,7 +92,31 @@ namespace JS { public struct Value { [CCode (cname = "JSValueGetType", instance_pos = 1.1)] - public JS.Type get_type(JS.Context context); + public Type get_type(Context context); + + [CCode (cname = "JSValueIsBoolean", instance_pos = 1.1)] + public bool is_boolean(Context ctx); + + [CCode (cname = "JSValueIsNumber", instance_pos = 1.1)] + public bool is_number(Context ctx); + + [CCode (cname = "JSValueIsObject", instance_pos = 1.1)] + public bool is_object(Context ctx); + + [CCode (cname = "JSValueIsString", instance_pos = 1.1)] + public bool is_string(Context ctx); + + [CCode (cname = "JSValueToBoolean", instance_pos = 1.1)] + public bool to_boolean(Context ctx); + + [CCode (cname = "JSValueToNumber", instance_pos = 1.1)] + public double to_number(Context ctx, out Value exception); + + [CCode (cname = "JSValueToObject", instance_pos = 1.1)] + public Object to_object(Context ctx, out Value exception); + + [CCode (cname = "JSValueToStringCopy", instance_pos = 1.1)] + public String to_string_copy(Context ctx, out Value exception); } @@ -108,26 +144,4 @@ namespace JS { } - [CCode (cname = "JSType", has_type_id = false)] - public enum Type { - - [CCode (cname = "kJSTypeUndefined")] - UNDEFINED, - - [CCode (cname = "kJSTypeNull")] - NULL, - - [CCode (cname = "kJSTypeBoolean")] - BOOLEAN, - - [CCode (cname = "kJSTypeNumber")] - NUMBER, - - [CCode (cname = "kJSTypeString")] - STRING, - - [CCode (cname = "kJSTypeObject")] - OBJECT - } - } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 63e7a2f1..dfab7bf2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -298,6 +298,7 @@ engine/util/util-html.vala engine/util/util-imap-utf7.vala engine/util/util-inet.vala engine/util/util-iterable.vala +engine/util/util-js.vala engine/util/util-numeric.vala engine/util/util-object.vala engine/util/util-reference-semantics.vala @@ -404,6 +405,7 @@ client/util/util-gravatar.vala client/util/util-gtk.vala client/util/util-international.vala client/util/util-migrate.vala +client/util/util-webkit.vala ) set(WEB_PROCESS_SRC @@ -522,6 +524,7 @@ set(ENGINE_PACKAGES gio-2.0 glib-2.0 gmime-2.6 + javascriptcore-4.0 libxml-2.0 posix sqlite3 @@ -533,7 +536,6 @@ set(CLIENT_PACKAGES geary-engine gio-2.0 gtk+-3.0 - javascriptcore-4.0 libcanberra libnotify libsecret-1 @@ -645,6 +647,7 @@ add_custom_target(webkit2gtk-vapi DEPENDS "${CMAKE_BINARY_DIR}/src/webkit2gtk-4.0.vapi" "${CMAKE_BINARY_DIR}/src/webkit2gtk-web-extension-4.0.vapi" + "${CMAKE_SOURCE_DIR}/bindings/vapi/javascriptcore-4.0.vapi" ) add_custom_command( OUTPUT diff --git a/src/client/components/client-web-view.vala b/src/client/components/client-web-view.vala index f886cc7f..a0564c57 100644 --- a/src/client/components/client-web-view.vala +++ b/src/client/components/client-web-view.vala @@ -6,8 +6,6 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -protected errordomain JSError { EXCEPTION, TYPE } - public class ClientWebView : WebKit.WebView { @@ -105,43 +103,6 @@ public class ClientWebView : WebKit.WebView { ); } - protected static bool get_bool_result(WebKit.JavascriptResult result) - throws JSError { - JS.GlobalContext context = result.get_global_context(); - JS.Value value = result.get_value(); - return context.to_boolean(value); - // XXX unref result? - } - - protected static int get_int_result(WebKit.JavascriptResult result) - throws JSError { - JS.GlobalContext context = result.get_global_context(); - JS.Value value = result.get_value(); - if (!context.is_number(value)) { - throw new JSError.TYPE("Value is not a number"); - } - JS.Value? err = null; - return (int) context.to_number(value, out err); - // XXX check err - // XXX unref result? - } - - protected static string? get_string_result(WebKit.JavascriptResult result) - throws JSError { - JS.GlobalContext context = result.get_global_context(); - JS.Value js_str_value = result.get_value(); - JS.Value? err = null; - JS.String js_str = context.to_string_copy(js_str_value, out err); - // XXX check err - int len = js_str.get_maximum_utf8_cstring_size(); - string value = string.nfill(len, 0); - js_str.get_utf8_cstring(value, len); - js_str.release(); - debug("Got string: %s", value); - return value; - // XXX unref result? - } - private static inline uint to_wk2_font_size(Pango.FontDescription font) { Gdk.Screen? screen = Gdk.Screen.get_default(); double dpi = screen != null ? screen.get_resolution() : 96.0; @@ -245,9 +206,9 @@ public class ClientWebView : WebKit.WebView { content_manager.script_message_received[PREFERRED_HEIGHT_MESSAGE].connect( (result) => { try { - this.preferred_height = get_int_result(result); + this.preferred_height = (int) WebKitUtil.to_number(result); queue_resize(); - } catch (JSError err) { + } catch (Geary.JS.Error err) { debug("Could not get preferred height: %s", err.message); } }); @@ -258,8 +219,8 @@ public class ClientWebView : WebKit.WebView { content_manager.script_message_received[SELECTION_CHANGED_MESSAGE].connect( (result) => { try { - selection_changed(get_bool_result(result)); - } catch (JSError err) { + selection_changed(WebKitUtil.to_bool(result)); + } catch (Geary.JS.Error err) { debug("Could not get selection content: %s", err.message); } }); diff --git a/src/client/composer/composer-web-view.vala b/src/client/composer/composer-web-view.vala index b1567335..457fc5e9 100644 --- a/src/client/composer/composer-web-view.vala +++ b/src/client/composer/composer-web-view.vala @@ -200,7 +200,7 @@ public class ComposerWebView : ClientWebView { WebKit.JavascriptResult result = yield this.run_javascript( "geary.getHtml();", null ); - return get_string_result(result); + return WebKitUtil.to_string(result); } /** @@ -210,7 +210,7 @@ public class ComposerWebView : ClientWebView { WebKit.JavascriptResult result = yield this.run_javascript( "geary.getText();", null ); - return get_string_result(result); + return WebKitUtil.to_string(result); } /** diff --git a/src/client/conversation-viewer/conversation-web-view.vala b/src/client/conversation-viewer/conversation-web-view.vala index a8373cca..03e7f46b 100644 --- a/src/client/conversation-viewer/conversation-web-view.vala +++ b/src/client/conversation-viewer/conversation-web-view.vala @@ -44,7 +44,7 @@ public class ConversationWebView : ClientWebView { WebKit.JavascriptResult result = yield this.run_javascript( "geary.getSelectionForFind();", null ); - return get_string_result(result); + return WebKitUtil.to_string(result); } /** @@ -54,7 +54,7 @@ public class ConversationWebView : ClientWebView { WebKit.JavascriptResult result = yield this.run_javascript( "geary.getSelectionForQuoting();", null ); - return get_string_result(result); + return WebKitUtil.to_string(result); } } diff --git a/src/client/util/util-webkit.vala b/src/client/util/util-webkit.vala new file mode 100644 index 00000000..cfce6f37 --- /dev/null +++ b/src/client/util/util-webkit.vala @@ -0,0 +1,87 @@ +/* + * Copyright 2017 Michael James Gratton + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +/** + * Utility functions for WebKit objects. + */ +namespace WebKitUtil { + + /** + * Returns a WebKit {@link WebKit.JavascriptResult} as a `bool`. + * + * This will raise a {@link Geary.JS.Error.TYPE} error if the + * result is not a JavaScript `Boolean`. + */ + public bool to_bool(WebKit.JavascriptResult result) + throws Geary.JS.Error { + JS.GlobalContext context = result.get_global_context(); + JS.Value value = result.get_value(); + if (!value.is_boolean(context)) { + throw new Geary.JS.Error.TYPE("Result is not a JS Boolean object"); + } + return value.to_boolean(context); + } + + /** + * Returns a WebKit {@link WebKit.JavascriptResult} as a `double`. + * + * This will raise a {@link Geary.JS.Error.TYPE} error if the + * result is not a JavaScript `Number`. + */ + public double to_number(WebKit.JavascriptResult result) + throws Geary.JS.Error { + JS.GlobalContext context = result.get_global_context(); + JS.Value value = result.get_value(); + if (!value.is_number(context)) { + throw new Geary.JS.Error.TYPE("Result is not a JS Number object"); + } + + JS.Value? err = null; + double number = value.to_number(context, out err); + Geary.JS.check_exception(context, err); + return number; + } + + /** + * Returns a WebKit {@link WebKit.JavascriptResult} as a Vala {@link string}. + * + * This will raise a {@link Geary.JS.Error.TYPE} error if the + * result is not a JavaScript `String`. + */ + public string? to_string(WebKit.JavascriptResult result) + throws Geary.JS.Error { + JS.GlobalContext context = result.get_global_context(); + JS.Value js_str_value = result.get_value(); + if (!js_str_value.is_string(context)) { + throw new Geary.JS.Error.TYPE("Result is not a JS String object"); + } + + JS.Value? err = null; + JS.String js_str = js_str_value.to_string_copy(context, out err); + Geary.JS.check_exception(context, err); + + return Geary.JS.to_string_released(js_str); + } + + /** + * Converts a WebKit {@link WebKit.JavascriptResult} to a {@link string}. + * + * Unlike the other `get_foo_result` methods, this will coax the + * result to a string, effectively by calling the JavaScript + * `toString()` method on it, and returning that value. + */ + public string? as_string(WebKit.JavascriptResult result) + throws Geary.JS.Error { + JS.GlobalContext context = result.get_global_context(); + JS.Value js_str_value = result.get_value(); + JS.Value? err = null; + JS.String js_str = js_str_value.to_string_copy(context, out err); + Geary.JS.check_exception(context, err); + return Geary.JS.to_string_released(js_str); + } + +} diff --git a/src/client/web-process/web-process-extension.vala b/src/client/web-process/web-process-extension.vala index 3dbbad11..23728e42 100644 --- a/src/client/web-process/web-process-extension.vala +++ b/src/client/web-process/web-process-extension.vala @@ -81,33 +81,65 @@ public class GearyWebExtension : Object { } private bool should_load_remote_images(WebKit.WebPage page) { + bool should_load = false; WebKit.Frame frame = page.get_main_frame(); JS.GlobalContext context = frame.get_javascript_global_context(); - JS.Value ret = execute_script(context, "geary.allowRemoteImages"); - // XXX check err here, log it - //JS.Value? err; - //return context.to_boolean(ret, err); - return context.to_boolean(ret); + try { + JS.Value ret = execute_script( + context, "geary.allowRemoteImages", int.parse("__LINE__") + ); + should_load = ret.to_boolean(context); + } catch (Error err) { + debug( + "Error checking PageState::allowRemoteImages: %s", + err.message + ); + } + return should_load; } private void remote_image_load_blocked(WebKit.WebPage page) { WebKit.Frame frame = page.get_main_frame(); - JS.Context context = frame.get_javascript_global_context(); - execute_script(context, "geary.remoteImageLoadBlocked();"); + JS.GlobalContext context = frame.get_javascript_global_context(); + try { + execute_script( + context, "geary.remoteImageLoadBlocked();", int.parse("__LINE__") + ); + } catch (Error err) { + debug( + "Error calling PageState::remoteImageLoadBlocked: %s", + err.message + ); + } } private void selection_changed(WebKit.WebPage page) { WebKit.Frame frame = page.get_main_frame(); - JS.Context context = frame.get_javascript_global_context(); - execute_script(context, "geary.selectionChanged();"); + JS.GlobalContext context = frame.get_javascript_global_context(); + try { + execute_script( + context, "geary.selectionChanged();", int.parse("__LINE__") + ); + } catch (Error err) { + debug("Error calling PageStates::selectionChanged: %s", err.message); + } } - private JS.Value execute_script(JS.Context context, string script) { + private JS.Value execute_script(JS.Context context, string script, int line) + throws Geary.JS.Error { JS.String js_script = new JS.String.create_with_utf8_cstring(script); - // XXX check err here, log it - //JS.Value? err; - //context.evaluate_script(js_script, null, null, 0, out err); - return context.evaluate_script(js_script, null, null, 0, null); + JS.String js_source = new JS.String.create_with_utf8_cstring("__FILE__"); + JS.Value? err = null; + try { + JS.Value ret = context.evaluate_script( + js_script, null, js_source, line, out err + ); + Geary.JS.check_exception(context, err); + return ret; + } finally { + js_script.release(); + js_source.release(); + } } } diff --git a/src/engine/util/util-js.vala b/src/engine/util/util-js.vala new file mode 100644 index 00000000..ddcf8d4f --- /dev/null +++ b/src/engine/util/util-js.vala @@ -0,0 +1,79 @@ +/* + * Copyright 2017 Michael James Gratton + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +/** + * Utility functions for WebKit JavaScriptCore (JSC) objects. + */ +namespace Geary.JS { + + /** + * Errors produced by functions in {@link Geary.JS}. + */ + public errordomain Error { + /** + * A JS exception was thrown performing a function call. + */ + EXCEPTION, + + /** + * A {@link JS.Value} was not of the expected type. + */ + TYPE + } + + /** + * Determines if a {@link JS.Value} object is {{{null}}}. + * + * @return `true` if `js` is `null` or has a {@link JS.Type} of + * `NULL` according to `context`. + */ + public inline bool is_null(global::JS.Context context, + global::JS.Value? js) { + return (js == null || js.get_type(context) == global::JS.Type.NULL); + } + + /** + * Returns a JSC {@link JS.String} as a Vala {@link string}. + */ + public inline string to_string_released(global::JS.String js) { + int len = js.get_maximum_utf8_cstring_size(); + string str = string.nfill(len, 0); + js.get_utf8_cstring(str, len); + js.release(); + return str; + } + + /** + * Checks an JS exception returned from a JSC call. + * + * This method will raise a {@link Geary.JS.Error} if the given + * `err_value` is not null (in a Vala or JS sense). + */ + public inline void check_exception(global::JS.Context context, + global::JS.Value? err_value) + throws Error { + if (!is_null(context, err_value)) { + global::JS.Value? nested_err = null; + global::JS.Type err_type = err_value.get_type(context); + global::JS.String err_str = + err_value.to_string_copy(context, out nested_err); + + if (!is_null(context, nested_err)) { + throw new Error.EXCEPTION( + "Nested exception getting exception %s as a string", + err_type.to_string() + ); + } + + throw new Error.EXCEPTION( + "JS exception thrown [%s]: %s" + .printf(err_type.to_string(), to_string_released(err_str)) + ); + } + } + +} diff --git a/test/js/composer-page-state-test.vala b/test/js/composer-page-state-test.vala index 1b8a41c4..485c6955 100644 --- a/test/js/composer-page-state-test.vala +++ b/test/js/composer-page-state-test.vala @@ -41,8 +41,8 @@ class ComposerPageStateTest : Gee.TestCase { load_body_fixture(html); try { assert(run_javascript(@"window.geary.getHtml();") == html + "

"); - } catch (JSError err) { - print("JSError: %s", err.message); + } catch (Geary.JS.Error err) { + print("Geary.JS.Error: %s", err.message); assert_not_reached(); } catch (Error err) { print("WKError: %s", err.message); @@ -54,8 +54,8 @@ class ComposerPageStateTest : Gee.TestCase { load_body_fixture("

para

"); try { assert(run_javascript(@"window.geary.getText();") == "para\n\n\n\n\n"); - } catch (JSError err) { - print("JSError: %s", err.message); + } catch (Geary.JS.Error err) { + print("Geary.JS.Error: %s", err.message); assert_not_reached(); } catch (Error err) { print("WKError: %s", err.message); @@ -68,8 +68,8 @@ class ComposerPageStateTest : Gee.TestCase { try { assert(run_javascript(@"window.geary.getText();") == "pre\n\n> quote\n> \npost\n\n\n\n\n"); - } catch (JSError err) { - print("JSError: %s", err.message); + } catch (Geary.JS.Error err) { + print("Geary.JS.Error: %s", err.message); assert_not_reached(); } catch (Error err) { print("WKError: %s", err.message); @@ -82,8 +82,8 @@ class ComposerPageStateTest : Gee.TestCase { try { assert(run_javascript(@"window.geary.getText();") == "pre\n\n> quote1\n> \n>> quote2\n>> \npost\n\n\n\n\n"); - } catch (JSError err) { - print("JSError: %s", err.message); + } catch (Geary.JS.Error err) { + print("Geary.JS.Error: %s", err.message); assert_not_reached(); } catch (Error err) { print("WKError: %s", err.message); @@ -116,8 +116,8 @@ class ComposerPageStateTest : Gee.TestCase { @"foo\n$(q_marker)quote1\nbar"); assert(run_javascript(@"ComposerPageState.resolveNesting('$(js_cosy_quote2)', $(js_values));") == @"foo\n$(q_marker)quote1\n$(q_marker)quote2\nbar"); - } catch (JSError err) { - print("JSError: %s", err.message); + } catch (Geary.JS.Error err) { + print("Geary.JS.Error: %s", err.message); assert_not_reached(); } catch (Error err) { print("WKError: %s", err.message); @@ -135,8 +135,8 @@ class ComposerPageStateTest : Gee.TestCase { @"$(q_marker)line1"); assert(run_javascript("ComposerPageState.quoteLines('line1\\nline2');") == @"$(q_marker)line1\n$(q_marker)line2"); - } catch (JSError err) { - print("JSError: %s", err.message); + } catch (Geary.JS.Error err) { + print("Geary.JS.Error: %s", err.message); assert_not_reached(); } catch (Error err) { print("WKError: %s", err.message); @@ -158,7 +158,7 @@ class ComposerPageStateTest : Gee.TestCase { WebKit.JavascriptResult result = this.test_view.run_javascript.end(async_result()); - return get_string_result(result); + return WebKitUtil.to_string(result); } protected void async_complete(AsyncResult result) { @@ -174,48 +174,4 @@ class ComposerPageStateTest : Gee.TestCase { return result; } - protected static string? get_string_result(WebKit.JavascriptResult result) - throws JSError { - JS.GlobalContext context = result.get_global_context(); - JS.Value js_str_value = result.get_value(); - JS.Value? err = null; - JS.String js_str = context.to_string_copy(js_str_value, out err); - - check_exception(context, err); - return to_string_released(js_str); - } - - protected static inline void check_exception(JS.Context exe, JS.Value? err_value) - throws JSError { - if (!is_null(exe, err_value)) { - JS.Value? nested_err = null; - JS.Type err_type = err_value.get_type(exe); - JS.String err_str = exe.to_string_copy(err_value, out nested_err); - - if (!is_null(exe, nested_err)) { - throw new JSError.EXCEPTION( - "Nested exception getting exception %s as a string", - err_type.to_string() - ); - } - - throw new JSError.EXCEPTION( - "JS exception thrown [%s]: %s" - .printf(err_type.to_string(), to_string_released(err_str)) - ); - } - } - - protected static inline bool is_null(JS.Context exe, JS.Value? js) { - return (js == null || js.get_type(exe) == JS.Type.NULL); - } - - protected static string to_string_released(JS.String js) { - int len = js.get_maximum_utf8_cstring_size(); - string str = string.nfill(len, 0); - js.get_utf8_cstring(str, len); - js.release(); - return str; - } - }