diff --git a/FFmpeg.AutoGen.Abstractions.Test/ConstCharPtrMarshalerTest.cs b/FFmpeg.AutoGen.Abstractions.Test/ConstCharPtrMarshalerTest.cs new file mode 100644 index 00000000..83ace8c9 --- /dev/null +++ b/FFmpeg.AutoGen.Abstractions.Test/ConstCharPtrMarshalerTest.cs @@ -0,0 +1,99 @@ +using System.Runtime.InteropServices; +using System.Text; + +namespace FFmpeg.AutoGen.Abstractions.Test +{ + /// + /// Tests for , the custom marshaler used for + /// const char* return values coming from FFmpeg. It must decode the native + /// UTF-8 string without ever freeing the (borrowed/static) native memory. + /// + /// This project multi-targets net9.0 and net48 so both compile-time branches of the + /// marshaler are covered: net9.0 references the netstandard2.1 build + /// (Marshal.PtrToStringUTF8) and net48 references the netstandard2.0 build + /// (the manual UTF-8 decode fallback). + /// + [TestClass] + public class ConstCharPtrMarshalerTest + { + private readonly List _allocations = new(); + + private static ICustomMarshaler Marshaler => ConstCharPtrMarshaler.GetInstance(null!); + + [TestCleanup] + public void Cleanup() + { + foreach (var ptr in _allocations) + Marshal.FreeHGlobal(ptr); + _allocations.Clear(); + } + + /// Allocates a NUL-terminated UTF-8 C string in unmanaged memory. + private IntPtr AllocUtf8(string value) + { + var bytes = Encoding.UTF8.GetBytes(value); + var ptr = Marshal.AllocHGlobal(bytes.Length + 1); + _allocations.Add(ptr); + Marshal.Copy(bytes, 0, ptr, bytes.Length); + Marshal.WriteByte(ptr, bytes.Length, 0); // NUL terminator + return ptr; + } + + [TestMethod] + public void GetInstance_ReturnsNonNullMarshaler() + { + Assert.IsNotNull(Marshaler); + } + + [TestMethod] + public void NullPointer_ReturnsNull() + { + var result = Marshaler.MarshalNativeToManaged(IntPtr.Zero); + Assert.IsNull(result); + } + + [TestMethod] + public void EmptyString_ReturnsEmptyString() + { + var ptr = AllocUtf8(string.Empty); + var result = Marshaler.MarshalNativeToManaged(ptr); + Assert.AreEqual(string.Empty, result); + } + + [TestMethod] + public void AsciiString_RoundTrips() + { + const string expected = "libavcodec 60.31.102"; + var ptr = AllocUtf8(expected); + var result = Marshaler.MarshalNativeToManaged(ptr); + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void Utf8MultiByteString_RoundTrips() + { + // Mix of 2-byte (é), 3-byte (日本語) and 4-byte (emoji) UTF-8 sequences. + const string expected = "café — 日本語 🎬"; + var ptr = AllocUtf8(expected); + var result = Marshaler.MarshalNativeToManaged(ptr); + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void CleanUpNativeData_DoesNotInvalidatePointer() + { + const string expected = "av_version_info"; + var ptr = AllocUtf8(expected); + + // CleanUpNativeData must be a no-op for borrowed FFmpeg memory: it must not + // free or mutate the buffer. So decoding again after it returns the same + // value from the still-valid pointer. + var first = Marshaler.MarshalNativeToManaged(ptr); + Marshaler.CleanUpNativeData(ptr); // documented no-op for borrowed memory + var second = Marshaler.MarshalNativeToManaged(ptr); + + Assert.AreEqual(expected, first); + Assert.AreEqual(expected, second); + } + } +} diff --git a/FFmpeg.AutoGen.Abstractions.Test/FFmpeg.AutoGen.Abstractions.Test.csproj b/FFmpeg.AutoGen.Abstractions.Test/FFmpeg.AutoGen.Abstractions.Test.csproj new file mode 100644 index 00000000..b6721d94 --- /dev/null +++ b/FFmpeg.AutoGen.Abstractions.Test/FFmpeg.AutoGen.Abstractions.Test.csproj @@ -0,0 +1,37 @@ + + + + + net9.0 + net9.0;net48 + enable + enable + false + preview + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/FFmpeg.AutoGen.Abstractions.Test/Usings.cs b/FFmpeg.AutoGen.Abstractions.Test/Usings.cs new file mode 100644 index 00000000..540383dc --- /dev/null +++ b/FFmpeg.AutoGen.Abstractions.Test/Usings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; diff --git a/FFmpeg.AutoGen.Abstractions/ConstCharPtrMarshaler.cs b/FFmpeg.AutoGen.Abstractions/ConstCharPtrMarshaler.cs index 47bd8e71..41ca9c94 100644 --- a/FFmpeg.AutoGen.Abstractions/ConstCharPtrMarshaler.cs +++ b/FFmpeg.AutoGen.Abstractions/ConstCharPtrMarshaler.cs @@ -6,7 +6,27 @@ namespace FFmpeg.AutoGen.Abstractions; public class ConstCharPtrMarshaler : ICustomMarshaler { private static readonly ConstCharPtrMarshaler Instance = new(); - public object MarshalNativeToManaged(IntPtr pNativeData) => Marshal.PtrToStringAnsi(pNativeData); + + public object MarshalNativeToManaged(IntPtr pNativeData) + { +#if NETSTANDARD2_1_OR_GREATER + return Marshal.PtrToStringUTF8(pNativeData); +#else + if (pNativeData == IntPtr.Zero) + return null; + + var length = 0; + while (Marshal.ReadByte(pNativeData, length) != 0) + length++; + + if (length == 0) + return string.Empty; + + var buffer = new byte[length]; + Marshal.Copy(pNativeData, buffer, 0, length); + return System.Text.Encoding.UTF8.GetString(buffer); +#endif + } public IntPtr MarshalManagedToNative(object managedObj) => IntPtr.Zero; diff --git a/FFmpeg.AutoGen.CppSharpUnsafeGenerator/Processing/FunctionProcessor.cs b/FFmpeg.AutoGen.CppSharpUnsafeGenerator/Processing/FunctionProcessor.cs index 5ae286e9..e14486f0 100644 --- a/FFmpeg.AutoGen.CppSharpUnsafeGenerator/Processing/FunctionProcessor.cs +++ b/FFmpeg.AutoGen.CppSharpUnsafeGenerator/Processing/FunctionProcessor.cs @@ -12,8 +12,6 @@ internal class FunctionProcessor { private const string ReturnMarshalAsConstCharPtr = "[return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(ConstCharPtrMarshaler))]"; - private const string ReturnMarshalAsLPUTF8Str = "[return: MarshalAs(UnmanagedType.LPUTF8Str)]"; - private const string MarshalAsUTF8Macros = " \r\n" + " #if NETSTANDARD2_1_OR_GREATER\r\n" + @@ -169,10 +167,17 @@ private TypeDefinition GetReturnType(Type type, string name) { return builtinType.Type switch { + // For const char* return values we must always use ConstCharPtrMarshaler and never + // [return: MarshalAs(UnmanagedType.LPUTF8Str)]. LPUTF8Str on a return value makes the + // CLR call CoTaskMemFree on the pointer after copying the string, but FFmpeg returns + // pointers to static/borrowed memory (e.g. av_version_info, avcodec_get_name, + // av_get_pix_fmt_name, AVClass names). Freeing that memory corrupts the native heap + // and leads to AccessViolationException. The NoCustomStringMarshal option only makes + // sense for parameters, where the CLR owns the marshalled buffer. PrimitiveType.Char => new TypeDefinition { Name = "string", - Attributes = new[] { _context.NoCustomStringMarshal ? ReturnMarshalAsLPUTF8Str : ReturnMarshalAsConstCharPtr } + Attributes = new[] { ReturnMarshalAsConstCharPtr } }, PrimitiveType.Void => new TypeDefinition { diff --git a/FFmpeg.AutoGen.sln b/FFmpeg.AutoGen.sln index 0fb39f25..e4b7e306 100644 --- a/FFmpeg.AutoGen.sln +++ b/FFmpeg.AutoGen.sln @@ -38,48 +38,138 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "3. Legacy", "3. Legacy", "{ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "2. Abstractions", "2. Abstractions", "{1B9CC2D2-096F-4D35-8940-DA6344450430}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FFmpeg.AutoGen.Abstractions.Test", "FFmpeg.AutoGen.Abstractions.Test\FFmpeg.AutoGen.Abstractions.Test.csproj", "{931DC899-6676-4884-A46C-060A4786DE78}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {A536B02A-B0B1-4753-8328-17369EF09976}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A536B02A-B0B1-4753-8328-17369EF09976}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A536B02A-B0B1-4753-8328-17369EF09976}.Debug|x64.ActiveCfg = Debug|Any CPU + {A536B02A-B0B1-4753-8328-17369EF09976}.Debug|x64.Build.0 = Debug|Any CPU + {A536B02A-B0B1-4753-8328-17369EF09976}.Debug|x86.ActiveCfg = Debug|Any CPU + {A536B02A-B0B1-4753-8328-17369EF09976}.Debug|x86.Build.0 = Debug|Any CPU {A536B02A-B0B1-4753-8328-17369EF09976}.Release|Any CPU.ActiveCfg = Release|Any CPU {A536B02A-B0B1-4753-8328-17369EF09976}.Release|Any CPU.Build.0 = Release|Any CPU + {A536B02A-B0B1-4753-8328-17369EF09976}.Release|x64.ActiveCfg = Release|Any CPU + {A536B02A-B0B1-4753-8328-17369EF09976}.Release|x64.Build.0 = Release|Any CPU + {A536B02A-B0B1-4753-8328-17369EF09976}.Release|x86.ActiveCfg = Release|Any CPU + {A536B02A-B0B1-4753-8328-17369EF09976}.Release|x86.Build.0 = Release|Any CPU {5BD870D6-27B4-4208-ACBF-496F2809326A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5BD870D6-27B4-4208-ACBF-496F2809326A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5BD870D6-27B4-4208-ACBF-496F2809326A}.Debug|x64.ActiveCfg = Debug|Any CPU + {5BD870D6-27B4-4208-ACBF-496F2809326A}.Debug|x64.Build.0 = Debug|Any CPU + {5BD870D6-27B4-4208-ACBF-496F2809326A}.Debug|x86.ActiveCfg = Debug|Any CPU + {5BD870D6-27B4-4208-ACBF-496F2809326A}.Debug|x86.Build.0 = Debug|Any CPU {5BD870D6-27B4-4208-ACBF-496F2809326A}.Release|Any CPU.ActiveCfg = Release|Any CPU {5BD870D6-27B4-4208-ACBF-496F2809326A}.Release|Any CPU.Build.0 = Release|Any CPU + {5BD870D6-27B4-4208-ACBF-496F2809326A}.Release|x64.ActiveCfg = Release|Any CPU + {5BD870D6-27B4-4208-ACBF-496F2809326A}.Release|x64.Build.0 = Release|Any CPU + {5BD870D6-27B4-4208-ACBF-496F2809326A}.Release|x86.ActiveCfg = Release|Any CPU + {5BD870D6-27B4-4208-ACBF-496F2809326A}.Release|x86.Build.0 = Release|Any CPU {2A8E06C6-5A68-4FB4-AE0C-F43B644E3737}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2A8E06C6-5A68-4FB4-AE0C-F43B644E3737}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2A8E06C6-5A68-4FB4-AE0C-F43B644E3737}.Debug|x64.ActiveCfg = Debug|Any CPU + {2A8E06C6-5A68-4FB4-AE0C-F43B644E3737}.Debug|x64.Build.0 = Debug|Any CPU + {2A8E06C6-5A68-4FB4-AE0C-F43B644E3737}.Debug|x86.ActiveCfg = Debug|Any CPU + {2A8E06C6-5A68-4FB4-AE0C-F43B644E3737}.Debug|x86.Build.0 = Debug|Any CPU {2A8E06C6-5A68-4FB4-AE0C-F43B644E3737}.Release|Any CPU.ActiveCfg = Release|Any CPU {2A8E06C6-5A68-4FB4-AE0C-F43B644E3737}.Release|Any CPU.Build.0 = Release|Any CPU + {2A8E06C6-5A68-4FB4-AE0C-F43B644E3737}.Release|x64.ActiveCfg = Release|Any CPU + {2A8E06C6-5A68-4FB4-AE0C-F43B644E3737}.Release|x64.Build.0 = Release|Any CPU + {2A8E06C6-5A68-4FB4-AE0C-F43B644E3737}.Release|x86.ActiveCfg = Release|Any CPU + {2A8E06C6-5A68-4FB4-AE0C-F43B644E3737}.Release|x86.Build.0 = Release|Any CPU {4557AF4F-4680-4764-A7A0-F739664B4DA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4557AF4F-4680-4764-A7A0-F739664B4DA1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4557AF4F-4680-4764-A7A0-F739664B4DA1}.Debug|x64.ActiveCfg = Debug|Any CPU + {4557AF4F-4680-4764-A7A0-F739664B4DA1}.Debug|x64.Build.0 = Debug|Any CPU + {4557AF4F-4680-4764-A7A0-F739664B4DA1}.Debug|x86.ActiveCfg = Debug|Any CPU + {4557AF4F-4680-4764-A7A0-F739664B4DA1}.Debug|x86.Build.0 = Debug|Any CPU {4557AF4F-4680-4764-A7A0-F739664B4DA1}.Release|Any CPU.ActiveCfg = Release|Any CPU {4557AF4F-4680-4764-A7A0-F739664B4DA1}.Release|Any CPU.Build.0 = Release|Any CPU + {4557AF4F-4680-4764-A7A0-F739664B4DA1}.Release|x64.ActiveCfg = Release|Any CPU + {4557AF4F-4680-4764-A7A0-F739664B4DA1}.Release|x64.Build.0 = Release|Any CPU + {4557AF4F-4680-4764-A7A0-F739664B4DA1}.Release|x86.ActiveCfg = Release|Any CPU + {4557AF4F-4680-4764-A7A0-F739664B4DA1}.Release|x86.Build.0 = Release|Any CPU {07F3A6D4-5599-4D77-9197-A666E8FC8EEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {07F3A6D4-5599-4D77-9197-A666E8FC8EEC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07F3A6D4-5599-4D77-9197-A666E8FC8EEC}.Debug|x64.ActiveCfg = Debug|Any CPU + {07F3A6D4-5599-4D77-9197-A666E8FC8EEC}.Debug|x64.Build.0 = Debug|Any CPU + {07F3A6D4-5599-4D77-9197-A666E8FC8EEC}.Debug|x86.ActiveCfg = Debug|Any CPU + {07F3A6D4-5599-4D77-9197-A666E8FC8EEC}.Debug|x86.Build.0 = Debug|Any CPU {07F3A6D4-5599-4D77-9197-A666E8FC8EEC}.Release|Any CPU.ActiveCfg = Release|Any CPU {07F3A6D4-5599-4D77-9197-A666E8FC8EEC}.Release|Any CPU.Build.0 = Release|Any CPU + {07F3A6D4-5599-4D77-9197-A666E8FC8EEC}.Release|x64.ActiveCfg = Release|Any CPU + {07F3A6D4-5599-4D77-9197-A666E8FC8EEC}.Release|x64.Build.0 = Release|Any CPU + {07F3A6D4-5599-4D77-9197-A666E8FC8EEC}.Release|x86.ActiveCfg = Release|Any CPU + {07F3A6D4-5599-4D77-9197-A666E8FC8EEC}.Release|x86.Build.0 = Release|Any CPU {1294EC3A-9667-40A2-8189-C9EC12ECFDD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1294EC3A-9667-40A2-8189-C9EC12ECFDD6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1294EC3A-9667-40A2-8189-C9EC12ECFDD6}.Debug|x64.ActiveCfg = Debug|Any CPU + {1294EC3A-9667-40A2-8189-C9EC12ECFDD6}.Debug|x64.Build.0 = Debug|Any CPU + {1294EC3A-9667-40A2-8189-C9EC12ECFDD6}.Debug|x86.ActiveCfg = Debug|Any CPU + {1294EC3A-9667-40A2-8189-C9EC12ECFDD6}.Debug|x86.Build.0 = Debug|Any CPU {1294EC3A-9667-40A2-8189-C9EC12ECFDD6}.Release|Any CPU.ActiveCfg = Release|Any CPU {1294EC3A-9667-40A2-8189-C9EC12ECFDD6}.Release|Any CPU.Build.0 = Release|Any CPU + {1294EC3A-9667-40A2-8189-C9EC12ECFDD6}.Release|x64.ActiveCfg = Release|Any CPU + {1294EC3A-9667-40A2-8189-C9EC12ECFDD6}.Release|x64.Build.0 = Release|Any CPU + {1294EC3A-9667-40A2-8189-C9EC12ECFDD6}.Release|x86.ActiveCfg = Release|Any CPU + {1294EC3A-9667-40A2-8189-C9EC12ECFDD6}.Release|x86.Build.0 = Release|Any CPU {ED2E790B-9233-40BC-BA66-E3291AAA9878}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {ED2E790B-9233-40BC-BA66-E3291AAA9878}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED2E790B-9233-40BC-BA66-E3291AAA9878}.Debug|x64.ActiveCfg = Debug|Any CPU + {ED2E790B-9233-40BC-BA66-E3291AAA9878}.Debug|x64.Build.0 = Debug|Any CPU + {ED2E790B-9233-40BC-BA66-E3291AAA9878}.Debug|x86.ActiveCfg = Debug|Any CPU + {ED2E790B-9233-40BC-BA66-E3291AAA9878}.Debug|x86.Build.0 = Debug|Any CPU {ED2E790B-9233-40BC-BA66-E3291AAA9878}.Release|Any CPU.ActiveCfg = Release|Any CPU {ED2E790B-9233-40BC-BA66-E3291AAA9878}.Release|Any CPU.Build.0 = Release|Any CPU + {ED2E790B-9233-40BC-BA66-E3291AAA9878}.Release|x64.ActiveCfg = Release|Any CPU + {ED2E790B-9233-40BC-BA66-E3291AAA9878}.Release|x64.Build.0 = Release|Any CPU + {ED2E790B-9233-40BC-BA66-E3291AAA9878}.Release|x86.ActiveCfg = Release|Any CPU + {ED2E790B-9233-40BC-BA66-E3291AAA9878}.Release|x86.Build.0 = Release|Any CPU {C272CD0A-E13B-4E80-8CA2-2BEC8396D8D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C272CD0A-E13B-4E80-8CA2-2BEC8396D8D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C272CD0A-E13B-4E80-8CA2-2BEC8396D8D0}.Debug|x64.ActiveCfg = Debug|Any CPU + {C272CD0A-E13B-4E80-8CA2-2BEC8396D8D0}.Debug|x64.Build.0 = Debug|Any CPU + {C272CD0A-E13B-4E80-8CA2-2BEC8396D8D0}.Debug|x86.ActiveCfg = Debug|Any CPU + {C272CD0A-E13B-4E80-8CA2-2BEC8396D8D0}.Debug|x86.Build.0 = Debug|Any CPU {C272CD0A-E13B-4E80-8CA2-2BEC8396D8D0}.Release|Any CPU.ActiveCfg = Release|Any CPU {C272CD0A-E13B-4E80-8CA2-2BEC8396D8D0}.Release|Any CPU.Build.0 = Release|Any CPU + {C272CD0A-E13B-4E80-8CA2-2BEC8396D8D0}.Release|x64.ActiveCfg = Release|Any CPU + {C272CD0A-E13B-4E80-8CA2-2BEC8396D8D0}.Release|x64.Build.0 = Release|Any CPU + {C272CD0A-E13B-4E80-8CA2-2BEC8396D8D0}.Release|x86.ActiveCfg = Release|Any CPU + {C272CD0A-E13B-4E80-8CA2-2BEC8396D8D0}.Release|x86.Build.0 = Release|Any CPU {028113A5-39CE-40A2-9118-56E99F30B4F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {028113A5-39CE-40A2-9118-56E99F30B4F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {028113A5-39CE-40A2-9118-56E99F30B4F8}.Debug|x64.ActiveCfg = Debug|Any CPU + {028113A5-39CE-40A2-9118-56E99F30B4F8}.Debug|x64.Build.0 = Debug|Any CPU + {028113A5-39CE-40A2-9118-56E99F30B4F8}.Debug|x86.ActiveCfg = Debug|Any CPU + {028113A5-39CE-40A2-9118-56E99F30B4F8}.Debug|x86.Build.0 = Debug|Any CPU {028113A5-39CE-40A2-9118-56E99F30B4F8}.Release|Any CPU.ActiveCfg = Release|Any CPU {028113A5-39CE-40A2-9118-56E99F30B4F8}.Release|Any CPU.Build.0 = Release|Any CPU + {028113A5-39CE-40A2-9118-56E99F30B4F8}.Release|x64.ActiveCfg = Release|Any CPU + {028113A5-39CE-40A2-9118-56E99F30B4F8}.Release|x64.Build.0 = Release|Any CPU + {028113A5-39CE-40A2-9118-56E99F30B4F8}.Release|x86.ActiveCfg = Release|Any CPU + {028113A5-39CE-40A2-9118-56E99F30B4F8}.Release|x86.Build.0 = Release|Any CPU + {931DC899-6676-4884-A46C-060A4786DE78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {931DC899-6676-4884-A46C-060A4786DE78}.Debug|Any CPU.Build.0 = Debug|Any CPU + {931DC899-6676-4884-A46C-060A4786DE78}.Debug|x64.ActiveCfg = Debug|Any CPU + {931DC899-6676-4884-A46C-060A4786DE78}.Debug|x64.Build.0 = Debug|Any CPU + {931DC899-6676-4884-A46C-060A4786DE78}.Debug|x86.ActiveCfg = Debug|Any CPU + {931DC899-6676-4884-A46C-060A4786DE78}.Debug|x86.Build.0 = Debug|Any CPU + {931DC899-6676-4884-A46C-060A4786DE78}.Release|Any CPU.ActiveCfg = Release|Any CPU + {931DC899-6676-4884-A46C-060A4786DE78}.Release|Any CPU.Build.0 = Release|Any CPU + {931DC899-6676-4884-A46C-060A4786DE78}.Release|x64.ActiveCfg = Release|Any CPU + {931DC899-6676-4884-A46C-060A4786DE78}.Release|x64.Build.0 = Release|Any CPU + {931DC899-6676-4884-A46C-060A4786DE78}.Release|x86.ActiveCfg = Release|Any CPU + {931DC899-6676-4884-A46C-060A4786DE78}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -92,6 +182,7 @@ Global {07F3A6D4-5599-4D77-9197-A666E8FC8EEC} = {E9CC1A7F-ADD1-486B-BEB3-B6E388B35731} {F969EEDD-27A8-434B-A438-39C48BBAEFE6} = {1B9CC2D2-096F-4D35-8940-DA6344450430} {1294EC3A-9667-40A2-8189-C9EC12ECFDD6} = {1B9CC2D2-096F-4D35-8940-DA6344450430} + {931DC899-6676-4884-A46C-060A4786DE78} = {1B9CC2D2-096F-4D35-8940-DA6344450430} {ED2E790B-9233-40BC-BA66-E3291AAA9878} = {F969EEDD-27A8-434B-A438-39C48BBAEFE6} {C272CD0A-E13B-4E80-8CA2-2BEC8396D8D0} = {F969EEDD-27A8-434B-A438-39C48BBAEFE6} {028113A5-39CE-40A2-9118-56E99F30B4F8} = {F969EEDD-27A8-434B-A438-39C48BBAEFE6} diff --git a/FFmpeg.AutoGen/ConstCharPtrMarshaler.cs b/FFmpeg.AutoGen/ConstCharPtrMarshaler.cs index 3bcca9c6..5424b7f5 100644 --- a/FFmpeg.AutoGen/ConstCharPtrMarshaler.cs +++ b/FFmpeg.AutoGen/ConstCharPtrMarshaler.cs @@ -6,7 +6,27 @@ namespace FFmpeg.AutoGen; public class ConstCharPtrMarshaler : ICustomMarshaler { private static readonly ConstCharPtrMarshaler Instance = new(); - public object MarshalNativeToManaged(IntPtr pNativeData) => Marshal.PtrToStringAnsi(pNativeData); + + public object MarshalNativeToManaged(IntPtr pNativeData) + { +#if NETSTANDARD2_1_OR_GREATER + return Marshal.PtrToStringUTF8(pNativeData); +#else + if (pNativeData == IntPtr.Zero) + return null; + + var length = 0; + while (Marshal.ReadByte(pNativeData, length) != 0) + length++; + + if (length == 0) + return string.Empty; + + var buffer = new byte[length]; + Marshal.Copy(pNativeData, buffer, 0, length); + return System.Text.Encoding.UTF8.GetString(buffer); +#endif + } public IntPtr MarshalManagedToNative(object managedObj) => IntPtr.Zero;