From e274b3355355d15008c122ff1d7d8d01b750f62f Mon Sep 17 00:00:00 2001 From: Devin Bidwell Date: Fri, 28 Nov 2025 14:44:26 -0700 Subject: [PATCH] wip -- marshal UTF16 string from C# to Rust to avoid GC in C# --- .../{SlangExtensions.cs => Extensions.cs} | 7 +++-- csharp_mod/{SlangGlue.cs => FfiGlue.cs} | 0 .../{SlangFormatter.cs => Formatter.cs} | 2 +- csharp_mod/Marshal.cs | 30 +++++++++++++++++++ csharp_mod/{SlangPlugin.cs => Plugin.cs} | 4 +-- rust_compiler/libs/parser/src/sys_call.rs | 4 +-- rust_compiler/src/lib.rs | 19 ++++++++---- 7 files changed, 52 insertions(+), 14 deletions(-) rename csharp_mod/{SlangExtensions.cs => Extensions.cs} (89%) rename csharp_mod/{SlangGlue.cs => FfiGlue.cs} (100%) rename csharp_mod/{SlangFormatter.cs => Formatter.cs} (76%) create mode 100644 csharp_mod/Marshal.cs rename csharp_mod/{SlangPlugin.cs => Plugin.cs} (96%) diff --git a/csharp_mod/SlangExtensions.cs b/csharp_mod/Extensions.cs similarity index 89% rename from csharp_mod/SlangExtensions.cs rename to csharp_mod/Extensions.cs index be39dcd..03b8426 100644 --- a/csharp_mod/SlangExtensions.cs +++ b/csharp_mod/Extensions.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Text; using StationeersIC10Editor; @@ -27,9 +26,11 @@ namespace Slang } // 2. Convert Rust Token Vector to C# List - public static List AsList(this Vec_FfiToken_t vec) + public static Line AsList(this Vec_FfiToken_t vec) { - var list = new List((int)vec.len); + var list = new Line(); + list.Capacity = (int)vec.len; + var currentPtr = vec.ptr; // Iterate through the raw memory array diff --git a/csharp_mod/SlangGlue.cs b/csharp_mod/FfiGlue.cs similarity index 100% rename from csharp_mod/SlangGlue.cs rename to csharp_mod/FfiGlue.cs diff --git a/csharp_mod/SlangFormatter.cs b/csharp_mod/Formatter.cs similarity index 76% rename from csharp_mod/SlangFormatter.cs rename to csharp_mod/Formatter.cs index fd96cba..6bd38fd 100644 --- a/csharp_mod/SlangFormatter.cs +++ b/csharp_mod/Formatter.cs @@ -6,7 +6,7 @@ namespace Slang { public override Line ParseLine(string line) { - throw new System.NotImplementedException(); + return Marshal.TokenizeLine(line); } } } diff --git a/csharp_mod/Marshal.cs b/csharp_mod/Marshal.cs new file mode 100644 index 0000000..83f283a --- /dev/null +++ b/csharp_mod/Marshal.cs @@ -0,0 +1,30 @@ +using System; +using System.Text; +using StationeersIC10Editor; + +namespace Slang +{ + public static class Marshal + { + public static unsafe Line TokenizeLine(string input) + { + if (String.IsNullOrEmpty(input)) + { + return new Line(); + } + + // Make sure the string is a null terminated string + if (input[input.Length - 1] != '\0') + { + input += '\0'; + } + + var strBytes = Encoding.UTF8.GetBytes(input); + + fixed (byte* ptrString = strBytes) + { + return Ffi.tokenize_line(ptrString).AsList(); + } + } + } +} diff --git a/csharp_mod/SlangPlugin.cs b/csharp_mod/Plugin.cs similarity index 96% rename from csharp_mod/SlangPlugin.cs rename to csharp_mod/Plugin.cs index 0822c23..78e5c67 100644 --- a/csharp_mod/SlangPlugin.cs +++ b/csharp_mod/Plugin.cs @@ -1,10 +1,9 @@ -using System; using System.IO; using System.Reflection; -using System.Runtime.InteropServices; using System.Text.RegularExpressions; using BepInEx; using HarmonyLib; +using StationeersIC10Editor; namespace Slang { @@ -103,6 +102,7 @@ namespace Slang ExtractNativeDll("slang.dll"); var harmony = new Harmony(PluginGuid); harmony.PatchAll(); + CodeFormatters.RegisterFormatter("slang", () => new SlangFormatter(), true); } private void ExtractNativeDll(string fileName) diff --git a/rust_compiler/libs/parser/src/sys_call.rs b/rust_compiler/libs/parser/src/sys_call.rs index bb2c882..576094f 100644 --- a/rust_compiler/libs/parser/src/sys_call.rs +++ b/rust_compiler/libs/parser/src/sys_call.rs @@ -124,7 +124,7 @@ pub enum System { /// Loads a LogicType from all connected network devices, aggregating them via a /// batchMode /// ## In Game - /// lb r? deviceHash loggicType batchMode + /// lb r? deviceHash logicType batchMode /// ## Examples /// lb r0 HASH("StructureWallLight") On Minimum LoadBatch(LiteralOrVariable, Literal, Literal), @@ -137,7 +137,7 @@ pub enum System { /// Represents a function which stores a setting to all devices that match /// the given deviceHash /// ## In Game - /// `sb deviceHash logictype r?` + /// `sb deviceHash logicType r?` /// ## Example /// `sb HASH("Doors") Lock 1` SetOnDeviceBatched(LiteralOrVariable, Literal, Box), diff --git a/rust_compiler/src/lib.rs b/rust_compiler/src/lib.rs index 2657211..991b421 100644 --- a/rust_compiler/src/lib.rs +++ b/rust_compiler/src/lib.rs @@ -14,11 +14,15 @@ pub struct FfiToken { pub column: i32, } +/// C# handles strings as UTF16. We do NOT want to allocate that memory in C# because +/// we want to avoid GC. So we pass it to Rust to handle all the memory allocations. +/// This should result in the ability to compile many times without triggering frame drops +/// from the GC from a `GetBytes()` call on a string in C#. #[ffi_export] -pub fn compile_from_string(input: safer_ffi::char_p::char_p_ref<'_>) -> safer_ffi::String { +pub fn compile_from_string(input: safer_ffi::slice::Ref<'_, u16>) -> safer_ffi::String { let mut writer = BufWriter::new(Vec::new()); - let tokenizer = Tokenizer::from(input.to_str()); + let tokenizer = Tokenizer::from(String::from_utf16_lossy(input.as_slice())); let parser = Parser::new(tokenizer); let compiler = Compiler::new(parser, &mut writer, None); @@ -33,10 +37,13 @@ pub fn compile_from_string(input: safer_ffi::char_p::char_p_ref<'_>) -> safer_ff // Safety: I know the compiler only outputs valid utf8 safer_ffi::String::from(unsafe { String::from_utf8_unchecked(compiled_vec) }) } - +/// C# handles strings as UTF16. We do NOT want to allocate that memory in C# because +/// we want to avoid GC. So we pass it to Rust to handle all the memory allocations. +/// This should result in the ability to tokenize many times without triggering frame drops +/// from the GC from a `GetBytes()` call on a string in C#. #[ffi_export] -pub fn tokenize_line(input: safer_ffi::char_p::char_p_ref<'_>) -> safer_ffi::Vec { - let tokenizer = Tokenizer::from(input.to_str()); +pub fn tokenize_line(input: safer_ffi::slice::Ref<'_, u16>) -> safer_ffi::Vec { + let tokenizer = Tokenizer::from(String::from_utf16_lossy(input.as_slice())); let mut tokens = Vec::::new(); @@ -83,6 +90,6 @@ pub fn free_string(s: safer_ffi::String) { pub fn generate_headers() -> std::io::Result<()> { ::safer_ffi::headers::builder() .with_language(safer_ffi::headers::Language::CSharp) - .to_file("../csharp_mod/SlangGlue.cs")? + .to_file("../csharp_mod/FfiGlue.cs")? .generate() }