Skip to content

Commit 3827ae5

Browse files
committed
Merge branch 'fix/utf8-string-marshaling-complete'
2 parents 896a839 + 0a9781d commit 3827ae5

5 files changed

Lines changed: 99 additions & 49 deletions

File tree

‎Function/BNCreateMetadataStringListData.cs‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@ internal static partial class NativeMethods
1717
EntryPoint = "BNCreateMetadataStringListData"
1818
)]
1919
internal static extern IntPtr BNCreateMetadataStringListData(
20-
21-
// const char** data
22-
string[] data ,
23-
20+
21+
// const char** data (UTF-8 block built by the caller; string[] cannot be
22+
// marshaled as UTF-8 by attribute, so it is passed as a raw pointer)
23+
IntPtr data ,
24+
2425
// uint64_t size
25-
ulong size
26+
ulong size
2627
);
2728
}
2829
}

‎Function/BNCreateMetadataValueStore.cs‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,16 @@ internal static partial class NativeMethods
1717
EntryPoint = "BNCreateMetadataValueStore"
1818
)]
1919
internal static extern IntPtr BNCreateMetadataValueStore(
20-
21-
// const char** keys
22-
string[] keys ,
23-
20+
21+
// const char** keys (UTF-8 block built by the caller; string[] cannot be
22+
// marshaled as UTF-8 by attribute, so it is passed as a raw pointer)
23+
IntPtr keys ,
24+
2425
// BNMetadata** values
25-
IntPtr[] values ,
26-
26+
IntPtr[] values ,
27+
2728
// uint64_t size
28-
ulong size
29+
ulong size
2930
);
3031
}
3132
}

‎Handle/BNMetadata.cs‎

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,10 @@ public Metadata(double[] value)
123123

124124
}
125125

126-
public Metadata(string[] value)
127-
: this(NativeMethods.BNCreateMetadataStringListData(value , (ulong)value.Length) , true)
126+
public Metadata(string[] value)
127+
: this(Metadata.rawCreateMetadataStringList(value) , true)
128128
{
129-
129+
130130
}
131131

132132
public Metadata(Metadata[] value)
@@ -225,12 +225,21 @@ private static IntPtr rawCreateMetadataDict(IDictionary<string,Metadata> dict)
225225
{
226226
handles.Add(item.DangerousGetHandle());
227227
}
228-
229-
return NativeMethods.BNCreateMetadataValueStore(
230-
dict.Keys.ToArray() ,
231-
handles.ToArray(),
232-
(ulong)handles.Count
233-
);
228+
229+
string[] keys = dict.Keys.ToArray();
230+
231+
// Keys are a UTF-8 char** block (see rawCreateMetadataStringList); the
232+
// core copies them, so freeing after the call is safe.
233+
using (ScopedAllocator allocator = new ScopedAllocator())
234+
{
235+
IntPtr keyBlock = allocator.AllocUtf8StringArray(keys);
236+
237+
return NativeMethods.BNCreateMetadataValueStore(
238+
keyBlock ,
239+
handles.ToArray() ,
240+
(ulong)handles.Count
241+
);
242+
}
234243
}
235244

236245
public static Metadata FromBoolArray(bool[] values)
@@ -264,9 +273,27 @@ public static Metadata FromDoubleArray(double[] values)
264273
public static Metadata FromStringArray(string[] values)
265274
{
266275
return Metadata.MustTakeHandle(
267-
NativeMethods.BNCreateMetadataStringListData(values , (ulong)values.Length)
276+
Metadata.rawCreateMetadataStringList(values)
268277
);
269278
}
279+
280+
// BN stores string lists as UTF-8 char** blocks. .NET cannot marshal a
281+
// string[] element as UTF-8 via [MarshalAs] (LPUTF8Str is rejected for an
282+
// array subtype), so build the char** block by hand. The core copies the
283+
// strings, so freeing the block after the call (ScopedAllocator dispose)
284+
// is safe.
285+
private static IntPtr rawCreateMetadataStringList(string[] values)
286+
{
287+
using (ScopedAllocator allocator = new ScopedAllocator())
288+
{
289+
IntPtr block = allocator.AllocUtf8StringArray(values);
290+
291+
return NativeMethods.BNCreateMetadataStringListData(
292+
block ,
293+
(ulong)values.Length
294+
);
295+
}
296+
}
270297

271298
internal static Metadata? NewFromHandle(IntPtr handle)
272299
{
@@ -425,7 +452,7 @@ public string[] ToStringList()
425452
out ulong arrayLength
426453
);
427454

428-
string[] values = UnsafeUtils.ReadAnsiStringArray(arrayPointer , arrayLength);
455+
string[] values = UnsafeUtils.ReadUtf8StringArray(arrayPointer , arrayLength);
429456

430457
if (arrayPointer != IntPtr.Zero )
431458
{
@@ -730,9 +757,9 @@ public bool SetValue(string key , double[] values)
730757
public bool SetValue(string key , string[] values)
731758
{
732759
return NativeMethods.BNMetadataSetValueForKey(
733-
this.handle,
734-
key,
735-
NativeMethods.BNCreateMetadataStringListData(values , (ulong)values.Length)
760+
this.handle,
761+
key,
762+
Metadata.rawCreateMetadataStringList(values)
736763
);
737764
}
738765

‎ScopedAllocator.cs‎

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,34 +38,51 @@ internal IntPtr AllocHGlobal(int size)
3838
return pointer;
3939
}
4040

41-
internal IntPtr AllocAnsiString(string text)
41+
// BN core expects UTF-8 for every string input. Encode into an HGlobal buffer
42+
// tracked by this allocator, so disposal keeps using Marshal.FreeHGlobal.
43+
internal IntPtr AllocUtf8String(string text)
4244
{
43-
IntPtr pointer = Marshal.StringToHGlobalAnsi(text);
44-
45-
m_pointers.Add(pointer);
45+
byte[] utf8 = Encoding.UTF8.GetBytes(text ?? string.Empty);
46+
47+
IntPtr pointer = this.AllocHGlobal(utf8.Length + 1);
48+
49+
Marshal.Copy(utf8 , 0 , pointer , utf8.Length);
50+
Marshal.WriteByte(pointer , utf8.Length , 0);
4651

4752
return pointer;
4853
}
49-
50-
internal IntPtr AllocAnsiStringArray(string[] texts)
54+
55+
internal IntPtr AllocUtf8StringArray(string[] texts)
5156
{
5257
if (texts == null || texts.Length == 0)
5358
{
5459
return IntPtr.Zero;
5560
}
5661

5762
IntPtr arrayPointer = this.AllocHGlobal(IntPtr.Size * texts.Length);
58-
63+
5964
for (int i = 0; i < texts.Length; i++)
6065
{
61-
IntPtr textPointer = this.AllocAnsiString(texts[i]);
62-
66+
IntPtr textPointer = this.AllocUtf8String(texts[i]);
67+
6368
Marshal.WriteIntPtr(arrayPointer , i * IntPtr.Size , textPointer);
6469
}
6570

6671
return arrayPointer;
6772
}
6873

74+
// The "Ansi"-named builders are misnomers retained for the generated call
75+
// sites; they delegate to the UTF-8 builders so every write path emits UTF-8.
76+
internal IntPtr AllocAnsiString(string text)
77+
{
78+
return this.AllocUtf8String(text);
79+
}
80+
81+
internal IntPtr AllocAnsiStringArray(string[] texts)
82+
{
83+
return this.AllocUtf8StringArray(texts);
84+
}
85+
6986
internal IntPtr AllocInteger<T>(T value) where T : IConvertible
7087
{
7188
int elementSize = Marshal.SizeOf<T>();

‎UnsafeUtils.cs‎

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -136,21 +136,15 @@ internal static T[] ReadNumberArray<T>(IntPtr arrayPointer , ulong arrayLength)
136136
return targets.ToArray();
137137
}
138138

139+
// BN core stores and returns every string as UTF-8 (the reference binding
140+
// decodes core strings with bytes.decode('utf8'); see the generator's
141+
// pyNativeStr helper). This reader and its Take/array variants all funnel
142+
// their per-element decode through here, so decoding as UTF-8 here fixes
143+
// every core-string reader at once. The "Ansi" name is a misnomer retained
144+
// only for the generated call sites that reference it.
139145
internal static string ReadAnsiString(IntPtr address)
140146
{
141-
string text = string.Empty;
142-
143-
if (IntPtr.Zero != address)
144-
{
145-
string? optional = Marshal.PtrToStringAnsi(address);
146-
147-
if (null != optional)
148-
{
149-
text = optional;
150-
}
151-
}
152-
153-
return text;
147+
return UnsafeUtils.ReadUtf8String(address);
154148
}
155149

156150
internal static string ReadUtf8String(IntPtr address)
@@ -595,9 +589,19 @@ internal static TNative[] ConvertToNativeArray<TNative,TManaged>(TManaged[] sour
595589
}
596590

597591

592+
// BN core expects UTF-8 for every string input. Encode into an HGlobal buffer
593+
// so the existing Marshal.FreeHGlobal ownership at the call sites is unchanged.
594+
// The "Ansi" name is a misnomer retained only for the generated call sites.
598595
internal static IntPtr AllocAnsiString(string text)
599596
{
600-
return Marshal.StringToHGlobalAnsi(text);
597+
byte[] utf8 = System.Text.Encoding.UTF8.GetBytes(text ?? string.Empty);
598+
599+
IntPtr pointer = Marshal.AllocHGlobal(utf8.Length + 1);
600+
601+
Marshal.Copy(utf8 , 0 , pointer , utf8.Length);
602+
Marshal.WriteByte(pointer , utf8.Length , 0);
603+
604+
return pointer;
601605
}
602606

603607
internal static IntPtr AllocUtf8String(string text)

0 commit comments

Comments
 (0)