diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/JsCore.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/JsCore.cs new file mode 100644 index 0000000..38db84a --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/JsCore.cs @@ -0,0 +1,171 @@ +// JsCore.cs +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors +// https://github.com/gnh1201/welsonjs +// +using System; +using System.Runtime.InteropServices; + +namespace WelsonJS.Launcher +{ + public sealed class JsCore : IDisposable + { + private IntPtr _runtime = IntPtr.Zero; + private IntPtr _context = IntPtr.Zero; + private bool _disposed; + + public JsCore() + { + Check(JsCreateRuntime(0, IntPtr.Zero, out _runtime), nameof(JsCreateRuntime)); + Check(JsCreateContext(_runtime, out _context), nameof(JsCreateContext)); + Check(JsSetCurrentContext(_context), nameof(JsSetCurrentContext)); + } + + /// + /// Evaluates JavaScript and returns the result converted to string (via JsConvertValueToString). + /// + public string EvaluateToString(string script, string sourceUrl = "repl") + { + if (_disposed) throw new ObjectDisposedException(nameof(JsCore)); + if (script is null) throw new ArgumentNullException(nameof(script)); + + Check(JsRunScript(script, IntPtr.Zero, sourceUrl, out var result), nameof(JsRunScript)); + + // Convert result -> JsString + Check(JsConvertValueToString(result, out var jsString), nameof(JsConvertValueToString)); + + // Extract pointer/length (UTF-16) and marshal to managed string + Check(JsStringToPointer(jsString, out var p, out var len), nameof(JsStringToPointer)); + return Marshal.PtrToStringUni(p, checked((int)len)); + } + + /// + /// Evaluates JavaScript for side effects; discards the result. + /// + public void Execute(string script, string sourceUrl = "repl") + { + _ = EvaluateToString(script, sourceUrl); + } + + private static void Check(JsErrorCode code, string op) + { + if (code != JsErrorCode.JsNoError) + throw new InvalidOperationException($"{op} failed with {code} (0x{(int)code:X})."); + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + try + { + // Unset the current context from the SAME physical thread that set it. + JsSetCurrentContext(IntPtr.Zero); + } + catch + { + // Swallow to ensure runtime is disposed. + } + finally + { + if (_runtime != IntPtr.Zero) + { + JsDisposeRuntime(_runtime); + _runtime = IntPtr.Zero; + } + _context = IntPtr.Zero; + } + + GC.SuppressFinalize(this); + } + + ~JsCore() => Dispose(); + + // ========================= + // P/Invoke surface (as given) + // ========================= + + // Essential (expanded) JsErrorCode set matching ChakraCore’s headers layout. + // Values are grouped by category bases (0x10000, 0x20000, ...). + public enum JsErrorCode + { + // Success + JsNoError = 0, + + // Category bases (useful when inspecting ranges) + JsErrorCategoryUsage = 0x10000, + JsErrorCategoryEngine = 0x20000, + JsErrorCategoryScript = 0x30000, + JsErrorCategoryFatal = 0x40000, + + // Usage errors (0x10001+) + JsErrorInvalidArgument = 0x10001, + JsErrorNullArgument = 0x10002, + JsErrorNoCurrentContext = 0x10003, + JsErrorInExceptionState = 0x10004, + JsErrorNotImplemented = 0x10005, + JsErrorWrongThread = 0x10006, + JsErrorRuntimeInUse = 0x10007, + JsErrorBadSerializedScript = 0x10008, + JsErrorInDisabledState = 0x10009, + JsErrorCannotDisableExecution = 0x1000A, + JsErrorHeapEnumInProgress = 0x1000B, + JsErrorArgumentNotObject = 0x1000C, + JsErrorInProfileCallback = 0x1000D, + JsErrorInThreadServiceCallback = 0x1000E, + JsErrorCannotSerializeDebugScript = 0x1000F, + JsErrorAlreadyDebuggingContext = 0x10010, + JsErrorAlreadyProfilingContext = 0x10011, + JsErrorIdleNotEnabled = 0x10012, + + // Engine errors (0x20001+) + JsErrorOutOfMemory = 0x20001, + JsErrorBadFPUState = 0x20002, + + // Script errors (0x30001+) + JsErrorScriptException = 0x30001, + JsErrorScriptCompile = 0x30002, + JsErrorScriptTerminated = 0x30003, + JsErrorScriptEvalDisabled = 0x30004, + + // Fatal (0x40001) + JsErrorFatal = 0x40001, + + // Misc/diagnostic (0x50000+) + JsErrorWrongRuntime = 0x50000, + JsErrorDiagAlreadyInDebugMode = 0x50001, + JsErrorDiagNotInDebugMode = 0x50002, + JsErrorDiagNotAtBreak = 0x50003, + JsErrorDiagInvalidHandle = 0x50004, + JsErrorDiagObjectNotFound = 0x50005, + JsErrorDiagUnableToPerformAction = 0x50006, + } + + [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.Cdecl)] + public static extern JsErrorCode JsCreateRuntime(uint attributes, IntPtr callback, out IntPtr runtime); + + [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.Cdecl)] + public static extern JsErrorCode JsCreateContext(IntPtr runtime, out IntPtr context); + + [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.Cdecl)] + public static extern JsErrorCode JsSetCurrentContext(IntPtr context); + + [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)] + public static extern JsErrorCode JsRunScript(string script, IntPtr sourceContext, string sourceUrl, out IntPtr result); + + [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.Cdecl)] + public static extern JsErrorCode JsConvertValueToString(IntPtr value, out IntPtr stringValue); + + [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.Cdecl)] + public static extern JsErrorCode JsStringToPointer(IntPtr value, out IntPtr buffer, out UIntPtr length); + + // Note: Unsetting is typically done via JsSetCurrentContext(IntPtr.Zero) + // Kept here only if your build exposes this symbol. + [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "JsSetCurrentContext")] + public static extern JsErrorCode JsUnSetCurrentContext(IntPtr zero); + + [DllImport("ChakraCore.dll", CallingConvention = CallingConvention.Cdecl)] + public static extern JsErrorCode JsDisposeRuntime(IntPtr runtime); + } +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/JsSerializer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/JsSerializer.cs new file mode 100644 index 0000000..b82b1a5 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/JsSerializer.cs @@ -0,0 +1,293 @@ +// JsSerializer.cs +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors +// https://github.com/gnh1201/welsonjs +// +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Text; + +namespace WelsonJS.Launcher +{ + public sealed class JsSerializer : IDisposable + { + private readonly JsCore _core; + private readonly bool _ownsCore; + + public JsSerializer() : this(new JsCore(), true) { } + + public JsSerializer(JsCore core, bool ownsCore) + { + if (core == null) throw new ArgumentNullException("core"); + _core = core; + _ownsCore = ownsCore; + } + + public bool IsValid(string json) + { + if (json == null) throw new ArgumentNullException("json"); + string script = + "(function(){try{JSON.parse(" + Q(json) + ");return '1';}catch(_){return '0';}})()"; + string r = _core.EvaluateToString(script); + return r == "1"; + } + + public string Minify(string json) + { + if (json == null) throw new ArgumentNullException("json"); + string script = "JSON.stringify(JSON.parse(" + Q(json) + "))"; + return _core.EvaluateToString(script); + } + + public string Pretty(string json, int space) + { + if (json == null) throw new ArgumentNullException("json"); + space = Clamp(space, 0, 10); + string script = "JSON.stringify(JSON.parse(" + Q(json) + "),null," + space.ToString(CultureInfo.InvariantCulture) + ")"; + return _core.EvaluateToString(script); + } + + public string Normalize(string json) + { + return Minify(json); + } + + /// + /// Extracts a value by a simple path of property names (numeric segment as string = array index). + /// Returns the selected value as a JSON string. + /// + public string Extract(string json, params string[] path) + { + if (path == null) path = new string[0]; + object[] mixed = new object[path.Length]; + for (int i = 0; i < path.Length; i++) mixed[i] = path[i]; + return Extract(json, mixed); + } + + /// + /// Extracts by a mixed path. Segments can be strings (object keys) or integers (array indices). + /// Returns the selected value as a JSON string (e.g., a JS string returns with quotes). + /// Usage: Extract(json, "items", 0, "name") + /// + public string Extract(string json, params object[] path) + { + if (json == null) throw new ArgumentNullException("json"); + if (path == null) path = new object[0]; + + string jsPath = BuildJsPath(path); + + var sb = new StringBuilder(); + sb.Append("(function(){var v=JSON.parse(").Append(Q(json)).Append(");"); + sb.Append("var p=").Append(jsPath).Append(";"); + sb.Append("for(var i=0;i(ReferenceEqualityComparer.Instance), 0); + string script = "JSON.stringify((" + expr + "),null," + space.ToString(CultureInfo.InvariantCulture) + ")"; + return _core.EvaluateToString(script); + } + + private static int Clamp(int v, int min, int max) + { + if (v < min) return min; + if (v > max) return max; + return v; + } + + /// + /// Encode a .NET string as a JS double-quoted string literal. + /// + private static string Q(string s) + { + if (s == null) return "null"; + var sb = new StringBuilder(s.Length + 16); + sb.Append('"'); + for (int i = 0; i < s.Length; i++) + { + char ch = s[i]; + switch (ch) + { + case '\\': sb.Append(@"\\"); break; + case '"': sb.Append("\\\""); break; + case '\b': sb.Append(@"\b"); break; + case '\f': sb.Append(@"\f"); break; + case '\n': sb.Append(@"\n"); break; + case '\r': sb.Append(@"\r"); break; + case '\t': sb.Append(@"\t"); break; + default: + if (char.IsControl(ch)) + { + sb.Append("\\u"); + sb.Append(((int)ch).ToString("X4")); + } + else + { + sb.Append(ch); + } + break; + } + } + sb.Append('"'); + return sb.ToString(); + } + + /// + /// Builds a JS array literal representing the path. + /// Numeric segments are emitted as numbers; others as strings. + /// + private static string BuildJsPath(object[] segments) + { + if (segments == null || segments.Length == 0) return "[]"; + + var sb = new StringBuilder(); + sb.Append('['); + for (int i = 0; i < segments.Length; i++) + { + if (i > 0) sb.Append(','); + + object seg = segments[i]; + + // Treat integral types as numbers for array indexing + if (seg is sbyte || seg is byte || + seg is short || seg is ushort || + seg is int || seg is uint || + seg is long || seg is ulong) + { + sb.Append(Convert.ToString(seg, CultureInfo.InvariantCulture)); + } + else + { + string str = (seg == null) ? string.Empty : Convert.ToString(seg, CultureInfo.InvariantCulture); + sb.Append(Q(str)); + } + } + sb.Append(']'); + return sb.ToString(); + } + + private static bool IsNumeric(object v) + { + if (v == null) return false; + Type t = v.GetType(); + t = Nullable.GetUnderlyingType(t) ?? t; + return t == typeof(byte) || t == typeof(sbyte) || + t == typeof(short) || t == typeof(ushort) || + t == typeof(int) || t == typeof(uint) || + t == typeof(long) || t == typeof(ulong) || + t == typeof(float) || t == typeof(double) || + t == typeof(decimal); + } + + private static bool IsImmutableLike(object v) + { + return v is string || v is bool || + v is byte || v is sbyte || + v is short || v is ushort || + v is int || v is uint || + v is long || v is ulong || + v is float || v is double || v is decimal || + v is Guid || v is DateTime || v is DateTimeOffset; + } + + /// + /// Builds a safe JS expression for a .NET value (no engine calls here). + /// Engine will stringify the produced expression via JSON.stringify. + /// + private static string BuildJsExpression(object value, HashSet seen, int depth) + { + if (depth > 64) return "null"; // depth guard + + if (value == null) return "null"; + + // Primitives + if (value is string) return Q((string)value); + if (value is bool) return ((bool)value) ? "true" : "false"; + if (IsNumeric(value)) + return Convert.ToString(value, CultureInfo.InvariantCulture); + + // Common value-like types → stringify as JS strings + if (value is Guid) + return Q(((Guid)value).ToString()); + if (value is DateTime) + return Q(((DateTime)value).ToUniversalTime().ToString("o", CultureInfo.InvariantCulture)); + if (value is DateTimeOffset) + return Q(((DateTimeOffset)value).ToUniversalTime().ToString("o", CultureInfo.InvariantCulture)); + if (value is byte[]) + return Q(Convert.ToBase64String((byte[])value)); + + // Prevent circular refs for reference types + if (!IsImmutableLike(value) && !seen.Add(value)) + return "null"; + + // IDictionary (string keys only) + if (value is IDictionary) + { + var map = (IDictionary)value; + var sb = new StringBuilder(); + sb.Append('{'); + bool first = true; + foreach (DictionaryEntry kv in map) + { + string key = kv.Key as string; + if (key == null) continue; // JSON keys must be strings + if (!first) sb.Append(','); + first = false; + sb.Append(Q(key)).Append(':') + .Append(BuildJsExpression(kv.Value, seen, depth + 1)); + } + sb.Append('}'); + return sb.ToString(); + } + + // IEnumerable → array + if (value is IEnumerable) + { + var seq = (IEnumerable)value; + var sb = new StringBuilder(); + sb.Append('['); + bool first = true; + foreach (object item in seq) + { + if (!first) sb.Append(','); + first = false; + sb.Append(BuildJsExpression(item, seen, depth + 1)); + } + sb.Append(']'); + return sb.ToString(); + } + + // Fallback → ToString() as JS string + string s = value.ToString(); + return Q(s ?? string.Empty); + } + + public void Dispose() + { + if (_ownsCore) + _core.Dispose(); + } + + /// + /// Reference equality comparer for cycle detection (works on .NET Framework). + /// + private sealed class ReferenceEqualityComparer : IEqualityComparer + { + private ReferenceEqualityComparer() { } + public static readonly ReferenceEqualityComparer Instance = new ReferenceEqualityComparer(); + bool IEqualityComparer.Equals(object x, object y) { return object.ReferenceEquals(x, y); } + int IEqualityComparer.GetHashCode(object obj) + { + return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj); + } + } + } +} \ No newline at end of file diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/NativeBootstrap.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/NativeBootstrap.cs new file mode 100644 index 0000000..1b91330 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/NativeBootstrap.cs @@ -0,0 +1,147 @@ +// NativeBootstrap.cs +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors +// https://github.com/gnh1201/welsonjs +// +using System; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Collections.Generic; + +namespace WelsonJS.Launcher +{ + public static class NativeBootstrap + { + // Win32 APIs + [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern IntPtr LoadLibrary(string lpFileName); + + [DllImport("kernel32", SetLastError = true)] + private static extern bool SetDefaultDllDirectories(int DirectoryFlags); + + [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "AddDllDirectory")] + private static extern IntPtr AddDllDirectory(string newDirectory); + + [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern bool SetDllDirectory(string lpPathName); + + private const int LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = 0x00001000; + + /// + /// Tries to load native libraries in the following order: + /// 1) %APPDATA%\{appDataSubdirectory}\{dllName} + /// 2) Application base directory\{dllName} + /// + /// Must be called before any P/Invoke usage. + /// + public static void Init(IEnumerable dllNames, string appDataSubdirectory, ICompatibleLogger logger) + { + if (dllNames == null) throw new ArgumentNullException(nameof(dllNames)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); + + string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + string appDataPath = string.IsNullOrEmpty(appDataSubdirectory) + ? appData + : Path.Combine(appData, appDataSubdirectory); + + string asmLocation = Assembly.GetEntryAssembly()?.Location + ?? Assembly.GetExecutingAssembly().Location + ?? AppContext.BaseDirectory; + string appBaseDirectory = Path.GetDirectoryName(asmLocation) ?? AppContext.BaseDirectory; + + var triedPaths = new List(); + + foreach (string dllName in dllNames) + { + if (string.IsNullOrWhiteSpace(dllName)) + continue; + + // 1) %APPDATA% subdirectory + string candidate1 = Path.Combine(appDataPath, dllName); + triedPaths.Add(candidate1); + if (TryLoad(candidate1, logger)) return; + + // 2) Application base directory + string candidate2 = Path.Combine(appBaseDirectory, dllName); + triedPaths.Add(candidate2); + if (TryLoad(candidate2, logger)) return; + } + + string message = "Failed to load requested native libraries.\n" + + "Tried:\n " + string.Join("\n ", triedPaths); + logger.Error(message); + throw new FileNotFoundException(message); + } + + private static bool TryLoad(string fullPath, ICompatibleLogger logger) + { + try + { + if (!File.Exists(fullPath)) + { + logger.Info($"Not found: {fullPath}"); + return false; + } + + string directoryPath = Path.GetDirectoryName(fullPath) ?? AppContext.BaseDirectory; + if (!TryRegisterSearchDirectory(directoryPath, logger)) + { + logger.Warn($"Could not register search directory: {directoryPath}"); + } + + logger.Info($"Loading: {fullPath}"); + IntPtr handle = LoadLibrary(fullPath); + if (handle == IntPtr.Zero) + { + int err = Marshal.GetLastWin32Error(); + logger.Warn($"LoadLibrary failed for {fullPath} (Win32Error={err})."); + return false; + } + + logger.Info($"Successfully loaded: {fullPath}"); + return true; + } + catch (Exception ex) + { + logger.Warn($"Exception while loading {fullPath}: {ex.Message}"); + return false; + } + } + + private static bool TryRegisterSearchDirectory(string directoryPath, ICompatibleLogger logger) + { + try + { + bool ok = SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); + if (!ok) + { + int e = Marshal.GetLastWin32Error(); + logger.Warn($"SetDefaultDllDirectories failed (Win32Error={e}), fallback to SetDllDirectory."); + return SetDllDirectory(directoryPath); + } + + IntPtr cookie = AddDllDirectory(directoryPath); + if (cookie == IntPtr.Zero) + { + int e = Marshal.GetLastWin32Error(); + logger.Warn($"AddDllDirectory failed (Win32Error={e}), fallback to SetDllDirectory."); + return SetDllDirectory(directoryPath); + } + + logger.Info($"Registered native DLL search directory: {directoryPath}"); + return true; + } + catch (EntryPointNotFoundException) + { + logger.Warn("DefaultDllDirectories API not available. Using SetDllDirectory fallback."); + return SetDllDirectory(directoryPath); + } + catch (Exception ex) + { + logger.Warn($"Register search directory failed for {directoryPath}: {ex.Message}"); + return false; + } + } + } +} \ No newline at end of file diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs index 23547c6..485a3f3 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs @@ -15,6 +15,7 @@ namespace WelsonJS.Launcher { internal static class Program { + private const string _appDataSubDirectory = "WelsonJS"; private static readonly ICompatibleLogger _logger; public static Mutex _mutex; @@ -22,7 +23,13 @@ namespace WelsonJS.Launcher static Program() { + // set up logger _logger = new TraceLogger(); + + // load native libraries + NativeBootstrap.Init(new string[] { + "ChakraCore.dll" + }, _appDataSubDirectory, _logger); } [STAThread] diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj index e9a04b4..c76f31e 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj @@ -88,6 +88,9 @@ + + +