1
0

Compare commits

..

2 Commits

Author SHA1 Message Date
c379c00a3f feat: update BMapSharp binding 2026-02-06 17:52:13 +08:00
37b8f2d023 feat: update BMapSharp marshaler 2026-02-06 17:13:28 +08:00
6 changed files with 983 additions and 810 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,303 @@
using System;
using System.Text;
using System.Runtime.InteropServices;
namespace BMapSharp.BMapMarshalers {
// References:
// https://stackoverflow.com/questions/18498452/how-do-i-write-a-custom-marshaler-which-allows-data-to-flow-from-native-to-manag
// https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-runtime-interopservices-icustommarshaler
//
// NOTE: I do not create a member to store the object we are marshaling.
// Because my binding do not have In, Out parameter. All parameters are In OR Out.
// So there is no reason to keep that member.
// YYC MARK:
// When receiving UTF8 string pointer given by BMap as managed string,
// I don't know why Microsoft try to call ICustomMarshaler.CleanUpNativeData without calling ICustomMarshaler.MarshalManagedToNative.
// It is trying to free the pointer managed by LibCmo self (for example, it will try to free we got string when getting object name)!
// So as the compromise, we introduce 2 different marshalers for In / Out string marshaling respectively.
// BMStringMarshaler for receiving string from BMap (OUT direction), and BMPOwnedStringMarshaler for passing string to BMap (IN direction).
// The name of marshaler for string array marshaling also following this pattern.
public class BMStringMarshaler : ICustomMarshaler {
private static readonly BMStringMarshaler INSTANCE = new BMStringMarshaler();
public static ICustomMarshaler GetInstance(string pstrCookie) {
return BMStringMarshaler.INSTANCE;
}
public IntPtr MarshalManagedToNative(object ManagedObj) {
// For OUT direction, we do not convert any managed data into native data.
// Return nullptr instead.
return IntPtr.Zero;
}
public object MarshalNativeToManaged(IntPtr pNativeData) {
// Check nullptr
if (pNativeData == IntPtr.Zero) return null;
// Call self
return BMStringMarshaler.ToManaged(pNativeData);
}
public void CleanUpNativeData(IntPtr pNativeData) {
// For OUT direction, we do not convert any managed data into native data.
// Do nothing here.
}
public void CleanUpManagedData(object ManagedObj) {
// Managed data will be cleaned by C# GC.
// So we do nothing here.
}
public int GetNativeDataSize() {
// Return -1 to indicate the size of the native data to be marshaled is variable.
return -1;
}
/// <summary>
/// Return the length in byte of given pointer represented C style string.
/// </summary>
/// <param name="ptr">The pointer for checking.</param>
/// <returns>The length of C style string (NUL exclusive).</returns>
internal static int GetCStringLength(IntPtr ptr) {
int count = 0, unit = Marshal.SizeOf<byte>();
while (Marshal.ReadByte(ptr) != (byte)0) {
ptr += unit;
++count;
}
return count;
}
/// <summary>
/// Extract managed string from given native pointer holding C style string data.
/// This function is shared by 2 marshalers.
/// </summary>
/// <param name="ptr">Native pointer holding string data. Caller must make sure this pointer is not nullptr.</param>
/// <returns>The extracted managed string data.</returns>
internal static string ToManaged(IntPtr ptr) {
// Get the length of given string.
int szStringItemCount = BMStringMarshaler.GetCStringLength(ptr);
int szStringItemSize = Marshal.SizeOf<byte>();
// Prepare cache and copy string data
byte[] encString = new byte[szStringItemCount];
Marshal.Copy(ptr, encString, 0, szStringItemCount);
// Decode string and return
return Encoding.UTF8.GetString(encString);
}
}
public class BMOwnedStringMarshaler : ICustomMarshaler {
private static readonly BMOwnedStringMarshaler INSTANCE = new BMOwnedStringMarshaler();
public static ICustomMarshaler GetInstance(string pstrCookie) {
return BMOwnedStringMarshaler.INSTANCE;
}
public IntPtr MarshalManagedToNative(object ManagedObj) {
// Check requirements.
if (ManagedObj is null) return IntPtr.Zero;
string castManagedObj = ManagedObj as string;
if (castManagedObj is null)
throw new MarshalDirectiveException("BMStringMarshaler must be used on a string.");
// Call self
return BMOwnedStringMarshaler.ToNative(castManagedObj);
}
public object MarshalNativeToManaged(IntPtr pNativeData) {
// For IN direction, we do not convert any native data into managed data.
// Return null instead.
return null;
}
public void CleanUpNativeData(IntPtr pNativeData) {
// Check nullptr
if (pNativeData == IntPtr.Zero) return;
// Free native pointer
Marshal.FreeHGlobal(pNativeData);
}
public void CleanUpManagedData(object ManagedObj) {
// For IN direction, we do not convert any native data into managed data.
// Do nothing here.
}
public int GetNativeDataSize() {
// Return -1 to indicate the size of the native data to be marshaled is variable.
return -1;
}
/// <summary>
/// Convert given string object to native data.
/// This function is shared by 2 marshalers.
/// </summary>
/// <param name="obj">String object. Caller must make sure this object is not null.</param>
/// <returns>The created native data pointer.</returns>
internal static IntPtr ToNative(string obj) {
// Encode string first
byte[] encString = Encoding.UTF8.GetBytes(obj);
// Allocate string memory with extra NUL.
int szStringItemCount = encString.Length;
int szStringItemSize = Marshal.SizeOf<byte>();
IntPtr pString = Marshal.AllocHGlobal(szStringItemSize * (szStringItemCount + 1));
// Copy encoded string data
Marshal.Copy(encString, 0, pString, szStringItemCount);
// Setup NUL
Marshal.WriteByte(pString + (szStringItemSize * szStringItemCount), (byte)0);
// Return value
return pString;
}
}
// YYC MARK:
// For respecting the standard of BMap,
// the native memory we created for string array is a simple array and each item is a pointer to a NULL-terminated UTF8 string.
// Please note the array self is also NULL-terminated otherwise we don't know its length.
public class BMStringArrayMarshaler : ICustomMarshaler {
private static readonly BMStringArrayMarshaler INSTANCE = new BMStringArrayMarshaler();
public static ICustomMarshaler GetInstance(string pstrCookie) {
return BMStringArrayMarshaler.INSTANCE;
}
public IntPtr MarshalManagedToNative(object ManagedObj) {
// For OUT direction, we do not convert any managed data into native data.
// Return nullptr instead.
return IntPtr.Zero;
}
public object MarshalNativeToManaged(IntPtr pNativeData) {
// Check nullptr
if (pNativeData == IntPtr.Zero) return null;
// Get the length of array
int szArrayItemCount = BMStringArrayMarshaler.GetArrayLength(pNativeData);
int szArrayItemSize = Marshal.SizeOf<IntPtr>();
// Prepare array cache and read it.
IntPtr[] apString = new IntPtr[szArrayItemCount];
Marshal.Copy(pNativeData, apString, 0, szArrayItemCount);
// Iterate the array and process each string one by one.
string[] ret = new string[szArrayItemCount];
for (int i = 0; i < szArrayItemCount; ++i) {
// Get string pointer
IntPtr pString = apString[i];
if (pString == IntPtr.Zero) {
ret[i] = null;
continue;
}
// Extract string
ret[i] = BMStringMarshaler.ToManaged(pString);
}
// Return result
return ret;
}
public void CleanUpNativeData(IntPtr pNativeData) {
// For OUT direction, we do not convert any managed data into native data.
// Do nothing here.
}
public void CleanUpManagedData(object ManagedObj) {
// Managed data will be cleaned by C# GC.
// So we do nothing here.
}
public int GetNativeDataSize() {
// Return -1 to indicate the size of the native data to be marshaled is variable.
return -1;
}
/// <summary>
/// Return the length of array created by this marshaler.
/// </summary>
/// <param name="ptr">The pointer to array for checking.</param>
/// <returns>The length of array (NULL terminal exclusive).</returns>
internal static int GetArrayLength(IntPtr ptr) {
int count = 0, unit = Marshal.SizeOf<IntPtr>();
while (Marshal.ReadIntPtr(ptr) != IntPtr.Zero) {
ptr += unit;
++count;
}
return count;
}
}
public class BMOwnedStringArrayMarshaler : ICustomMarshaler {
private static readonly BMOwnedStringArrayMarshaler INSTANCE = new BMOwnedStringArrayMarshaler();
public static ICustomMarshaler GetInstance(string pstrCookie) {
return BMOwnedStringArrayMarshaler.INSTANCE;
}
public IntPtr MarshalManagedToNative(object ManagedObj) {
// Check nullptr object.
if (ManagedObj is null) return IntPtr.Zero;
// Check argument type.
string[] castManagedObj = ManagedObj as string[];
if (castManagedObj is null)
throw new MarshalDirectiveException("BMStringArrayMashaler must be used on an string array.");
// Allocate string items first
int szArrayItemCount = castManagedObj.Length;
int szArrayItemSize = Marshal.SizeOf<IntPtr>();
IntPtr[] apString = new IntPtr[szArrayItemCount];
for (int i = 0; i < szArrayItemCount; ++i) {
// Check null string
string stringObj = castManagedObj[i];
if (stringObj is null) apString[i] = IntPtr.Zero;
else apString[i] = BMOwnedStringMarshaler.ToNative(stringObj);
}
// Allocate array pointer now.
IntPtr pArray = Marshal.AllocHGlobal(szArrayItemSize * (szArrayItemCount + 1));
// Copy string pointer data
Marshal.Copy(apString, 0, pArray, szArrayItemCount);
// Setup NULL ternimal
Marshal.WriteIntPtr(pArray + (szArrayItemSize * szArrayItemCount), IntPtr.Zero);
// Return value
return pArray;
}
public object MarshalNativeToManaged(IntPtr pNativeData) {
// For IN direction, we do not convert any native data into managed data.
// Return null instead.
return null;
}
public void CleanUpNativeData(IntPtr pNativeData) {
// Check nullptr
if (pNativeData == IntPtr.Zero) return;
// Get the length of array
int szArrayItemCount = BMStringArrayMarshaler.GetArrayLength(pNativeData);
int szArrayItemSize = Marshal.SizeOf<IntPtr>();
// Prepare array cache and read it.
IntPtr[] apString = new IntPtr[szArrayItemCount];
Marshal.Copy(pNativeData, apString, 0, szArrayItemCount);
// Free array self
Marshal.FreeHGlobal(pNativeData);
// Iterate the string pointer array and free them one by one.
foreach (IntPtr pString in apString) {
// Free string pointer
if (pString == IntPtr.Zero) continue;
Marshal.FreeHGlobal(pString);
}
}
public void CleanUpManagedData(object ManagedObj) {
// For IN direction, we do not convert any native data into managed data.
// Do nothing here.
}
public int GetNativeDataSize() {
// Return -1 to indicate the size of the native data to be marshaled is variable.
return -1;
}
}
}

View File

@@ -5,6 +5,20 @@ using BMapSharp.VirtoolsTypes;
namespace BMapSharp.BMapWrapper { namespace BMapSharp.BMapWrapper {
/// <summary>
/// BMapSharp module specific exception.
/// </summary>
public class BMapException : Exception {
public BMapException() { }
public BMapException(string message)
: base(message) { }
public BMapException(string message, Exception inner)
: base(message, inner) { }
public static void ThrowIfFailed(bool condition) {
if (!condition) throw new BMapException("BMap operation failed.");
}
}
/// <summary> /// <summary>
/// The guard of native BMap environment. /// The guard of native BMap environment.
/// This class initialize native BMap environment when constructing and free it when destructing. /// This class initialize native BMap environment when constructing and free it when destructing.

View File

@@ -14,7 +14,7 @@ def _get_libcmo21_repo_directory() -> Path:
def get_input_file_path() -> Path: def get_input_file_path() -> Path:
return _get_libcmo21_repo_directory() / "BMap" / "BMap" / "BMExports.hpp" return _get_libcmo21_repo_directory() / "Ballance" / "BMap" / "BMap" / "BMExports.hpp"
def _get_bmap_binder_directory() -> Path: def _get_bmap_binder_directory() -> Path:

View File

@@ -32,6 +32,7 @@ CPP_PY_TYPE_MAP: dict[str, str] = {
"VXSHADE_MODE": "enum", "VXSHADE_MODE": "enum",
"VXCMPFUNC": "enum", "VXCMPFUNC": "enum",
"VXMESH_LITMODE": "enum", "VXMESH_LITMODE": "enum",
"CK_CAMERA_PROJECTION": "enum",
} }
CS_ENUM_LIKE: set[str] = set(( CS_ENUM_LIKE: set[str] = set((
@@ -46,6 +47,7 @@ CS_ENUM_LIKE: set[str] = set((
"VXSHADE_MODE", "VXSHADE_MODE",
"VXCMPFUNC", "VXCMPFUNC",
"VXMESH_LITMODE", "VXMESH_LITMODE",
"CK_CAMERA_PROJECTION",
)) ))
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -108,10 +110,12 @@ class RenderUtils:
# only allow 0 and 1 pointer level for string. # only allow 0 and 1 pointer level for string.
match vt_pointer_level: match vt_pointer_level:
case 0: case 0:
marshal_as = f'UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(BMStringMarshaler), MarshalCookie = "{direction_cookie}"' marshaler = 'BMOwnedStringMarshaler' if param.is_input else 'BMStringMarshaler'
marshal_as = f'UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof({marshaler})'
cs_type = "string" cs_type = "string"
case 1: case 1:
marshal_as = f'UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(BMStringArrayMarshaler), MarshalCookie = "{direction_cookie}"' marshaler = 'BMOwnedStringArrayMarshaler' if param.is_input else 'BMStringArrayMarshaler'
marshal_as = f'UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof({marshaler})'
cs_type = "string[]" cs_type = "string[]"
case "CKDWORD": case "CKDWORD":
if vt_pointer_level == 0: if vt_pointer_level == 0:

View File

@@ -4,7 +4,7 @@
/// <param name="{{ param.var_name }}">Direction: {% if param.is_input -%} input {%- else -%} output {%- endif %}. C++ type: {{ param.var_type.to_c_type() }}. {{ param.var_desc }}</param> /// <param name="{{ param.var_name }}">Direction: {% if param.is_input -%} input {%- else -%} output {%- endif %}. C++ type: {{ param.var_type.to_c_type() }}. {{ param.var_desc }}</param>
{%- endfor %} {%- endfor %}
/// <returns>True if no error, otherwise False.</returns> /// <returns>True if no error, otherwise False.</returns>
[DllImport(g_DllName, EntryPoint = "{{ fct.fct_name }}", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi, ExactSpelling = true)] [DllImport(DLL_NAME, EntryPoint = "{{ fct.fct_name }}", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi, ExactSpelling = true)]
[return: MarshalAs(UnmanagedType.U1)] [return: MarshalAs(UnmanagedType.U1)]
internal static extern bool {{ fct.fct_name }}( internal static extern bool {{ fct.fct_name }}(
{%- for param in fct.fct_params -%} {%- for param in fct.fct_params -%}