diff --git a/src/Config.cs b/src/Config.cs index 03d7c0b..0a6fdd1 100644 --- a/src/Config.cs +++ b/src/Config.cs @@ -8,6 +8,8 @@ internal static class Config internal class ConfigData { public bool EnableEval { get; set; } = false; + + public bool EnableLogger { get; set; } = true; } internal static ConfigData Data { get; private set; } diff --git a/src/Hook/JsLog/ContextObjectBindPrint.cs b/src/Hook/JsLog/ContextObjectBindPrint.cs new file mode 100644 index 0000000..8b35463 --- /dev/null +++ b/src/Hook/JsLog/ContextObjectBindPrint.cs @@ -0,0 +1,25 @@ +using Hosihikari.NativeInterop.Hook.ObjectOriented; +using Hosihikari.VanillaScript.QuickJS; +using Hosihikari.VanillaScript.QuickJS.Types; + +namespace Hosihikari.VanillaScript.Hook.JsLog; + +internal class ContextObjectBindPrint : HookBase +{ + internal unsafe delegate JsValue HookDelegate(void* contextObject, JsContext* ctx); + + public ContextObjectBindPrint() + : base("_ZN9Scripting7QuickJS13ContextObject10_bindPrintEP9JSContext") { } + + public override unsafe HookDelegate HookedFunc => + (contextObject, ctx) => + { + if (Config.Data.EnableLogger) + { + using var globalObject = Native.JS_GetGlobalObject(ctx); + JsLog.Bind(ctx, globalObject.Value); + return globalObject.Value; + } + return Original.Invoke(contextObject, ctx); + }; +} diff --git a/src/Hook/JsLog/JsLog.cs b/src/Hook/JsLog/JsLog.cs new file mode 100644 index 0000000..9a4d741 --- /dev/null +++ b/src/Hook/JsLog/JsLog.cs @@ -0,0 +1,172 @@ +using System.Runtime.InteropServices; +using System.Text; +using Hosihikari.VanillaScript.QuickJS; +using Hosihikari.VanillaScript.QuickJS.Extensions; +using Hosihikari.VanillaScript.QuickJS.Helper; +using Hosihikari.VanillaScript.QuickJS.Types; + +namespace Hosihikari.VanillaScript.Hook.JsLog; + +internal class JsLog +{ + public static unsafe void Bind(JsContext* ctx, JsValue globalObject) + { + using var consoleInstance = Native.JS_NewObject(ctx, false); + var console = consoleInstance.Value; + #region Trace + console.DefineFunction(ctx, "trace", &PrintTrace, 1, flags: JsPropertyFlags.Normal); + [UnmanagedCallersOnly] + static JsValue PrintTrace(JsContext* ctx, JsValue thisObj, int argCount, JsValue* argvIn) + { + try + { + var (file, line) = GetJsSourceInfo(ctx); + Log.Logger.Trace( + ParseLog(ctx, new ReadOnlySpan(argvIn, argCount)), + sourceFile: file, + sourceLine: line + ); + } + catch (Exception ex) + { + Log.Logger.Error("PrintTrace", "Invoke Failed", ex); + } + return JsValueCreateHelper.Undefined; + } + #endregion + #region Info + globalObject.DefineFunction(ctx, "print", &PrintInfo, 1, flags: JsPropertyFlags.Normal); + console.DefineFunction(ctx, "log", &PrintInfo, 1, flags: JsPropertyFlags.Normal); + console.DefineFunction(ctx, "info", &PrintInfo, 1, flags: JsPropertyFlags.Normal); + [UnmanagedCallersOnly] + static JsValue PrintInfo(JsContext* ctx, JsValue thisObj, int argCount, JsValue* argvIn) + { + try + { + Log.Logger.Info(ParseLog(ctx, new ReadOnlySpan(argvIn, argCount))); + } + catch (Exception ex) + { + Log.Logger.Error("PrintInfo", "Invoke Failed", ex); + } + return JsValueCreateHelper.Undefined; + } + #endregion + #region Debug + console.DefineFunction(ctx, "debug", &PrintDebug, 1, flags: JsPropertyFlags.Normal); + [UnmanagedCallersOnly] + static JsValue PrintDebug(JsContext* ctx, JsValue thisObj, int argCount, JsValue* argvIn) + { + try + { + Log.Logger.Debug(ParseLog(ctx, new ReadOnlySpan(argvIn, argCount))); + } + catch (Exception ex) + { + Log.Logger.Error("PrintDebug", "Invoke Failed", ex); + } + return JsValueCreateHelper.Undefined; + } + #endregion + #region Warn + console.DefineFunction(ctx, "warn", &PrintWarn, 1, flags: JsPropertyFlags.Normal); + [UnmanagedCallersOnly] + static JsValue PrintWarn(JsContext* ctx, JsValue thisObj, int argCount, JsValue* argvIn) + { + try + { + Log.Logger.Warn(ParseLog(ctx, new ReadOnlySpan(argvIn, argCount))); + } + catch (Exception ex) + { + Log.Logger.Error("PrintWarn", "Invoke Failed", ex); + } + return JsValueCreateHelper.Undefined; + } + #endregion + #region Error + console.DefineFunction(ctx, "error", &PrintError, 1, flags: JsPropertyFlags.Normal); + [UnmanagedCallersOnly] + static JsValue PrintError(JsContext* ctx, JsValue thisObj, int argCount, JsValue* argvIn) + { + try + { + var (file, line) = GetJsSourceInfo(ctx); + Log.Logger.Error( + ParseLog(ctx, new ReadOnlySpan(argvIn, argCount)), + sourceFile: file, + sourceLine: line + ); + } + catch (Exception ex) + { + Log.Logger.Error("PrintError", "Invoke Failed", ex); + } + return JsValueCreateHelper.Undefined; + } + #endregion + #region Assert + //todo Assert + //console.DefineFunction(ctx, "assert", &Assert, 2, flags: JsPropertyFlags.Normal); + + //[UnmanagedCallersOnly] + //static JsValue Assert(JsContext* ctx, JsValue thisObj, int argCount, JsValue* argvIn) + //{ + // var argv = new ReadOnlySpan(argvIn, argCount); + // if (argv[0].IsFalsey(ctx)) + // { + // Log.Logger.Error(ParseLog(ctx, argv.Slice(1))); + // } + // return JsValueCreateHelper.Undefined; + //} + #endregion + #region Clear + console.DefineFunction(ctx, "clear", &Clear, 0, flags: JsPropertyFlags.Normal); + [UnmanagedCallersOnly] + static JsValue Clear(JsContext* ctx, JsValue thisObj, int argCount, JsValue* argvIn) + { + Console.Clear(); + return JsValueCreateHelper.Undefined; + } + #endregion + globalObject.DefineProperty(ctx, "console", console, flags: JsPropertyFlags.Normal); + } + + static unsafe (string file, int line) GetJsSourceInfo(JsContext* ctx) + { + try //get full stack from Error + { + Native.JS_ThrowError(ctx, ""); + var error = Native.JS_GetException(ctx); + var stack = error.Value.GetStringProperty(ctx, "stack"); + var lines = stack.Split('\n'); + var file = lines[1].Split(' ')[1]; + var line = int.Parse(lines[1].Split(' ')[2]); + return (file, line); + } + catch //get only file name from JS_GetScriptOrModuleName + { + var file = Native.JS_GetScriptOrModuleName(ctx, 1); + return (file, -1); + } + } + + static unsafe string ParseLog(JsContext* ctx, ReadOnlySpan argv) + { + var sb = new StringBuilder(); + foreach (var arg in argv) + { + sb.Append(arg.ToString(ctx)); + if (arg.Data.tag == JsTag.Object && Native.JS_IsError(ctx, arg)) + { + sb.AppendLine(); + sb.AppendLine(arg.GetStringProperty(ctx, "stack")); + } + else + { + sb.Append(" "); + } + } + return sb.ToString(); + } +} diff --git a/src/Hook/RequestReload.cs b/src/Hook/RequestReload.cs new file mode 100644 index 0000000..33a4578 --- /dev/null +++ b/src/Hook/RequestReload.cs @@ -0,0 +1,19 @@ +using Hosihikari.NativeInterop.Hook.ObjectOriented; + +namespace Hosihikari.VanillaScript.Hook +{ + internal class RequestReload : HookBase + { + internal unsafe delegate void HookDelegate(void* minecraft); + + public RequestReload() + : base("_ZN9Minecraft21requestResourceReloadEv") { } + + public override unsafe HookDelegate HookedFunc => + minecraft => + { + Original.Invoke(minecraft); + Config.Reload(); + }; + } +} diff --git a/src/Loader/CustomScriptLoader.cs b/src/Loader/CustomScriptLoader.cs index a59d5e9..48bdc8b 100644 --- a/src/Loader/CustomScriptLoader.cs +++ b/src/Loader/CustomScriptLoader.cs @@ -67,7 +67,7 @@ void LoadScript(string path) { var bytes = File.ReadAllText(path); var relativePath = Path.GetRelativePath(pluginsDir, path); - var ret = Native.JS_Eval(ctx, relativePath, bytes); + using var ret = Native.JS_Eval(ctx, relativePath, bytes); ScriptLoaded?.Invoke( (nint)ctx, new ScriptLoadedEventArgs( diff --git a/src/Main.cs b/src/Main.cs index f7500ce..9fdbb4b 100644 --- a/src/Main.cs +++ b/src/Main.cs @@ -15,6 +15,7 @@ public void Initialize(AssemblyPlugin plugin) new Hook.QuickJS.Eval().Install(); new Hook.QuickJS.FreeContext().Install(); new Hook.QuickJS.AddIntrinsicBaseObjects().Install(); + new Hook.JsLog.ContextObjectBindPrint().Install(); Assets.Prepare.Init(); } } diff --git a/src/QuickJS/Exceptions/QuickJSException.cs b/src/QuickJS/Exceptions/QuickJSException.cs index a7b7df5..9188bf8 100644 --- a/src/QuickJS/Exceptions/QuickJSException.cs +++ b/src/QuickJS/Exceptions/QuickJSException.cs @@ -31,6 +31,10 @@ public override string? StackTrace var originalStackTrace = base.StackTrace; if (JsStack is not null) { + if (originalStackTrace is null) + { + return JsStack; + } return JsStack + Environment.NewLine + originalStackTrace; } return originalStackTrace; diff --git a/src/QuickJS/Extensions/JsValueExtension.cs b/src/QuickJS/Extensions/JsValueExtension.cs index 77d3087..8b8155d 100644 --- a/src/QuickJS/Extensions/JsValueExtension.cs +++ b/src/QuickJS/Extensions/JsValueExtension.cs @@ -65,7 +65,7 @@ public static unsafe string GetStringProperty( string propertyName ) { - var val = Native.JS_GetPropertyStr(ctx, @this, propertyName); + using var val = Native.JS_GetPropertyStr(ctx, @this, propertyName); return Native.JS_ToCString(ctx, val.Value); } @@ -104,10 +104,19 @@ public static unsafe bool DefineFunction( int argumentLength, JscFunctionEnum cproto = JscFunctionEnum.Generic, int magic = 0, - JsPropertyFlags flags = JsPropertyFlags.CWE + JsPropertyFlags flags = JsPropertyFlags.CWE, + bool autoDrop = false ) { - var value = Native.JS_NewCFunction2(ctx, func, funcName, argumentLength, cproto, magic); + using var value = Native.JS_NewCFunction2( + ctx, + func, + funcName, + argumentLength, + cproto, + magic, + autoDrop + ); return Native.JS_DefinePropertyValueStr(ctx, @this, funcName, value.Value, flags); } diff --git a/src/QuickJS/Native.cs b/src/QuickJS/Native.cs index 14dc89e..9d8952a 100644 --- a/src/QuickJS/Native.cs +++ b/src/QuickJS/Native.cs @@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Hosihikari.NativeInterop; using Hosihikari.NativeInterop.Utils; using Hosihikari.VanillaScript.QuickJS.Exceptions; @@ -7,6 +8,7 @@ using Hosihikari.VanillaScript.QuickJS.Types; using Hosihikari.VanillaScript.QuickJS.Wrapper; using size_t = nuint; +using JsAtom = System.UInt32; namespace Hosihikari.VanillaScript.QuickJS; @@ -32,12 +34,11 @@ internal static void __JS_FreeValue(JsContext* ctx, JsValue jsValue) #region JS_GetGlobalObject public static AutoDropJsValue JS_GetGlobalObject(JsContext* ctx) { - JsValue ret = new(); - var func = (delegate* unmanaged)_ptrJsGetGlobalObject.Value; + var func = (delegate* unmanaged)_ptrJsGetGlobalObject.Value; //the call will increase refCount - var result = func(&ret, ctx); + var result = func(ctx); //return SafeJsValue to auto remove refCount - return new AutoDropJsValue(*result, ctx); + return new AutoDropJsValue(result, ctx); } private static readonly Lazy _ptrJsGetGlobalObject = GetPointerLazy("JS_GetGlobalObject"); @@ -286,7 +287,8 @@ public static AutoDropJsValue JS_NewCFunction2( string name, int argumentLength, //Note: at least 'length' arguments will be readable in 'argv' JscFunctionEnum cproto, - int magic + int magic, + bool autoDrop ) { fixed (byte* ptr = StringUtils.StringToManagedUtf8(name)) @@ -305,7 +307,7 @@ int magic { throw new QuickJsException(JS_GetException(ctx)); } - return new AutoDropJsValue(result, ctx); + return autoDrop ? new AutoDropJsValue(result, ctx) : new SafeJsValue(result, ctx); } } @@ -488,12 +490,9 @@ public static JsValue JS_ThrowError( { return JS_ThrowError( ctx, - exMessage.Replace( - "%", - "%%" - ) /* prevent format*/ - , + exMessage.Replace("%", "%%"), /* prevent format*/ errorType, + true, __arglist() ); } @@ -502,6 +501,7 @@ public static JsValue JS_ThrowError( JsContext* ctx, string exMessage, JsErrorEnum errorType = JsErrorEnum.InternalError, + bool addBackTrace = true, __arglist ) { @@ -512,9 +512,10 @@ public static JsValue JS_ThrowError( JsErrorEnum, byte*, RuntimeArgumentHandle, + int, JsValue>) _ptrJsThrowError.Value; - var result = func(ctx, errorType, ptr, __arglist); + var result = func(ctx, errorType, ptr, __arglist, addBackTrace ? 1 : 0); if (!result.IsException()) { //it seem always return exception type @@ -525,6 +526,58 @@ public static JsValue JS_ThrowError( } } - private static readonly Lazy _ptrJsThrowError = GetPointerLazy("JS_ThrowError"); + private static readonly Lazy _ptrJsThrowError = GetPointerLazy("JS_ThrowError2"); + #endregion + + #region JS_GetScriptOrModuleName + //JSAtom JS_GetScriptOrModuleName(JSContext *ctx, int n_stack_levels) + public static string JS_GetScriptOrModuleName(JsContext* ctx, int nStackLevels) + { + var func = (delegate* unmanaged)_ptrJsGetScriptOrModuleName.Value; + var result = func(ctx, nStackLevels); + if (result == JsAtomConst.Null) + { + return string.Empty; + } + try + { + return JS_AtomToCString(ctx, result); + } + finally + { + JS_FreeAtom(ctx, result); + } + } + + private static readonly Lazy _ptrJsGetScriptOrModuleName = GetPointerLazy( + "JS_GetScriptOrModuleName" + ); + #endregion + + #region JS_AtomToCString + + //const char *JS_AtomToCString(JSContext *ctx, JSAtom atom) + public static string JS_AtomToCString(JsContext* ctx, JsAtom atom) + { + var func = (delegate* unmanaged)_ptrJsAtomToCString.Value; + var result = func(ctx, atom); + if (result is null) + throw new QuickJsException(JS_GetException(ctx)); + JS_FreeCString(ctx, result); + return Marshal.PtrToStringUTF8((nint)result) ?? string.Empty; + } + + private static readonly Lazy _ptrJsAtomToCString = GetPointerLazy("JS_AtomToCString"); + #endregion + #region JS_FreeAtom + //void JS_FreeAtom(JSContext *ctx, JSAtom v) + public static void JS_FreeAtom(JsContext* ctx, JsAtom v) + { + var func = (delegate* unmanaged)_ptrJsFreeAtom.Value; + func(ctx, v); + } + + private static readonly Lazy _ptrJsFreeAtom = GetPointerLazy("JS_FreeAtom"); + #endregion } diff --git a/src/QuickJS/Types/JsAtom.cs b/src/QuickJS/Types/JsAtom.cs index 4150b80..a1effc4 100644 --- a/src/QuickJS/Types/JsAtom.cs +++ b/src/QuickJS/Types/JsAtom.cs @@ -1,7 +1,8 @@ namespace Hosihikari.VanillaScript.QuickJS.Types; -internal enum JsAtom : uint +public static class JsAtomConst { + public const int Null = 0; //DEF(null, "null") /* must be first */ //DEF(false, "false") //DEF(true, "true")