// 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);
}
}