Skip to content

Commit 2049d72

Browse files
committed
CSharp bindings: marshal all strings as UTF-8
Fix string marshalling with C# bindings so that all strings are marshalled to/from unmanaged UTF-8. - Replace SWIG's built-in `SWIGStringHelper` with a modified version named `Utf8StringHelper`. - Change C# and Java string constants from runtime to compile time. - Minor Refactor
1 parent 586707b commit 2049d72

6 files changed

Lines changed: 183 additions & 56 deletions

File tree

swig/csharp/CMakeLists.txt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,8 @@ list(APPEND GDAL_SWIG_COMMON_INTERFACE_FILES
137137
${PROJECT_SOURCE_DIR}/swig/include/csharp/ogr_csharp.i
138138
${PROJECT_SOURCE_DIR}/swig/include/csharp/osr_csharp.i
139139
${PROJECT_SOURCE_DIR}/swig/include/csharp/swig_csharp_extensions.i
140-
${PROJECT_SOURCE_DIR}/swig/include/csharp/typemaps_csharp.i)
140+
${PROJECT_SOURCE_DIR}/swig/include/csharp/typemaps_csharp.i
141+
${PROJECT_SOURCE_DIR}/swig/include/csharp/csharp_strings.i)
141142

142143
# function for csharp wrapper build
143144
function (gdal_csharp_wrap)
@@ -154,9 +155,10 @@ function (gdal_csharp_wrap)
154155
COMMAND ${CMAKE_COMMAND} -E make_directory ${_CSHARP_WORKING_DIRECTORY}
155156
# SWIG command
156157
COMMAND
157-
${SWIG_EXECUTABLE} -namespace ${_CSHARP_NAMESPACE} -outdir ${_CSHARP_WORKING_DIRECTORY} -DSWIG2_CSHARP -dllimport
158-
${_CSHARP_WRAPPER} -Wall -I${PROJECT_SOURCE_DIR}/swig/include -I${PROJECT_SOURCE_DIR}/swig/include/csharp
159-
-I${PROJECT_SOURCE_DIR}/gdal -c++ -csharp -o ${_CSHARP_WRAPPER}.cpp ${_CSHARP_SWIG_INTERFACE}
158+
${SWIG_EXECUTABLE} -namespace ${_CSHARP_NAMESPACE} -outdir ${_CSHARP_WORKING_DIRECTORY} -DSWIG2_CSHARP
159+
-DSWIG_CSHARP_NO_STRING_HELPER -dllimport ${_CSHARP_WRAPPER} -Wall -I${PROJECT_SOURCE_DIR}/swig/include
160+
-I${PROJECT_SOURCE_DIR}/swig/include/csharp -I${PROJECT_SOURCE_DIR}/gdal -c++ -csharp -o ${_CSHARP_WRAPPER}.cpp
161+
${_CSHARP_SWIG_INTERFACE}
160162
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
161163
DEPENDS ${GDAL_SWIG_COMMON_INTERFACE_FILES} ${PROJECT_SOURCE_DIR}/swig/include/csharp/typemaps_csharp.i
162164
${_CSHARP_SWIG_INTERFACE})

swig/csharp/apps/GDALTestUtf8.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@ public static void Main()
1616
RunTest("ThreadLocalConfigOption", Gdal.GetThreadLocalConfigOption, Gdal.SetThreadLocalConfigOption);
1717
RunTest("ThreadLocalConfigOption", (k, d) => Gdal.GetPathSpecificOption("/", k, d), (k, v) => Gdal.SetPathSpecificOption("/", k, v));
1818
RunTest("Credential", (k, d) => Gdal.GetCredential("/", k, d), (k, v) => Gdal.SetCredential("/", k, v));
19+
20+
XMLNode node = new XMLNode(XMLNodeType.CXT_Element, "TestElement");
21+
node.SetXMLValue(".", UnicodeString);
22+
var serializedXml = node.SerializeXMLTree();
23+
var expectedXml = $"<TestElement>{UnicodeString}</TestElement>";
24+
AssertEqual(expectedXml, serializedXml?.TrimEnd('\n'), $"{nameof(XMLNode)}.{nameof(node.SerializeXMLTree)}");
25+
26+
var gcp = new GCP(0, 0, 0, 0, 0, UnicodeString, "Id");
27+
AssertEqual(gcp.Info, UnicodeString, $"{nameof(GCP)}.{nameof(gcp.Info)}");
28+
gcp.Id = UnicodeString;
29+
AssertEqual(gcp.Id, UnicodeString, $"{nameof(GCP)}.{nameof(gcp.Id)}");
1930
}
2031

2132
private static void RunTest(string name, Func<string, string, string> getter, Action<string, string> setter)
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/******************************************************************************
2+
*
3+
* Name: csharp_strings.i
4+
* Project: GDAL CSharp Interface
5+
* Purpose: Typemaps for C# marshalling to/from UTF-8.
6+
* Authors: Tamas Szekeres, szekerest@gmail.com
7+
* Michael Bucari-Tovo, mbucari1@gmail.com
8+
*
9+
******************************************************************************
10+
* Copyright (c) 2007, Tamas Szekeres; 2026, Michael Bucari-Tovo
11+
*
12+
* SPDX-License-Identifier: MIT
13+
*****************************************************************************/
14+
15+
16+
%insert(runtime) %{
17+
#include <stdlib.h>
18+
#include <string.h>
19+
#include <stdio.h>
20+
%}
21+
22+
23+
/******************************************************************************
24+
* Marshaler for NULL terminated UTF-8 encoded strings *
25+
******************************************************************************/
26+
27+
%insert(runtime) %{
28+
/* Callback for returning strings to C# without leaking memory */
29+
typedef char * (SWIGSTDCALL* CSharpUtf8StringHelperCallback)(const char *);
30+
static CSharpUtf8StringHelperCallback SWIG_csharp_string_callback = NULL;
31+
%}
32+
33+
%pragma(csharp) imclasscode=%{
34+
public class Utf8StringHelper {
35+
36+
public delegate System.IntPtr Utf8StringDelegate(System.IntPtr message);
37+
38+
static Utf8StringDelegate stringDelegate = new Utf8StringDelegate($modulePINVOKE.Utf8UnmanagedToLengthPrefixedUtf16Unmanaged);
39+
40+
[global::System.Runtime.InteropServices.DllImport("$dllimport", EntryPoint="RegisterUtf8StringCallback_$module")]
41+
public static extern void RegisterUtf8StringCallback_$module(Utf8StringDelegate stringDelegate);
42+
43+
static Utf8StringHelper() {
44+
RegisterUtf8StringCallback_$module(stringDelegate);
45+
}
46+
}
47+
48+
public static readonly Utf8StringHelper utf8StringHelper = new Utf8StringHelper();
49+
50+
public unsafe static IntPtr Utf8UnmanagedToLengthPrefixedUtf16Unmanaged(IntPtr pUtf8Bts)
51+
{
52+
if (pUtf8Bts == IntPtr.Zero)
53+
return IntPtr.Zero;
54+
55+
byte* pStringUtf8 = (byte*)pUtf8Bts;
56+
int len = 0;
57+
while (pStringUtf8[len] != 0) len++;
58+
59+
var charCount = System.Text.Encoding.UTF8.GetCharCount(pStringUtf8, len);
60+
var bstrDest = Marshal.AllocHGlobal(sizeof(int) + charCount * sizeof(char));
61+
*(int*)bstrDest = charCount;
62+
63+
System.Text.Encoding.UTF8.GetChars(pStringUtf8, len, (char*)IntPtr.Add(bstrDest, sizeof(int)), charCount);
64+
return bstrDest;
65+
}
66+
67+
public unsafe static string LengthPrefixedUtf16UnmanagedToString(IntPtr pBstr)
68+
{
69+
if (pBstr == IntPtr.Zero)
70+
return null;
71+
72+
try
73+
{
74+
int charCount = *(int*)pBstr;
75+
if (charCount < 0)
76+
return null;
77+
int charStart = sizeof(int) / sizeof(char);
78+
return new string((char*)pBstr, charStart, charCount);
79+
}
80+
finally
81+
{
82+
Marshal.FreeHGlobal(pBstr);
83+
}
84+
}
85+
86+
public unsafe static IntPtr StringToUtf8Unmanaged(string str)
87+
{
88+
if (str == null)
89+
return IntPtr.Zero;
90+
91+
int byteCount = System.Text.Encoding.UTF8.GetByteCount(str);
92+
IntPtr unmanagedString = Marshal.AllocHGlobal(byteCount + 1);
93+
94+
byte* ptr = (byte*)unmanagedString.ToPointer();
95+
fixed (char *pStr = str)
96+
{
97+
System.Text.Encoding.UTF8.GetBytes(pStr, str.Length, ptr, byteCount);
98+
// null-terminate
99+
ptr[byteCount] = 0;
100+
}
101+
102+
return unmanagedString;
103+
}
104+
%}
105+
106+
%insert(runtime) %{
107+
#ifdef __cplusplus
108+
extern "C"
109+
#endif
110+
SWIGEXPORT void SWIGSTDCALL RegisterUtf8StringCallback_$module(CSharpUtf8StringHelperCallback callback) {
111+
SWIG_csharp_string_callback = callback;
112+
}
113+
%}
114+
115+
/******************************************************************************
116+
* Apply typemaps for all SWIG string types and for const char *utf8_string *
117+
******************************************************************************/
118+
119+
/* Changing csin and imtype typemaps for string types defined by SWIG in csharp.swg */
120+
121+
%typemap(imtype) (char *), (char *&), (char[ANY]), (char[]), (const char *utf8_string) "IntPtr"
122+
123+
%typemap(in) (char *), (char *&), (char[ANY]), (char[]), (const char *utf8_string) %{
124+
$1 = ($1_ltype)$input;
125+
%}
126+
127+
%typemap(out) (char *), (char *&), (char[ANY]), (char[]), (const char *utf8_string)
128+
%{
129+
$result = SWIG_csharp_string_callback((const char *)$1);
130+
%}
131+
132+
%typemap(csin) (char *), (char *&), (char[ANY]), (char[]), (const char *utf8_string) "$modulePINVOKE.StringToUtf8Unmanaged($csinput)"
133+
134+
%typemap(csout, excode=SWIGEXCODE) (char *), (char *&), (char[ANY]), (char[]), (const char *utf8_string)
135+
{
136+
/* %typemap(csout) (char *), (char *&), (char[ANY]), (char[]), (const char *utf8_string) */
137+
IntPtr cPtr = $imcall;
138+
string ret = $modulePINVOKE.LengthPrefixedUtf16UnmanagedToString(cPtr);
139+
$excode
140+
return ret;
141+
}
142+
143+
%typemap(csvarout, excode=SWIGEXCODE2) (char *), (char *&), (char[ANY]), (char[]), (const char *utf8_string) %{
144+
get {
145+
/* %typemap(csvarout) (char *), (char *&), (char[ANY]), (char[]), (const char *utf8_string) */
146+
IntPtr cPtr = $imcall;
147+
string ret = $modulePINVOKE.LengthPrefixedUtf16UnmanagedToString(cPtr);
148+
$excode
149+
return ret;
150+
} %}

swig/include/csharp/ogr_csharp.i

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ DEFINE_EXTERNAL_CLASS(GDALMajorObjectShadow, OSGeo.GDAL.MajorObject)
7171
return new $csclassname(wkbGeometryType.wkbUnknown, null, 0, IntPtr.Zero, gml);
7272
}
7373

74-
public Geometry(wkbGeometryType type) : this(OgrPINVOKE.new_Geometry((int)type, null, 0, IntPtr.Zero, null), true, null) {
74+
public Geometry(wkbGeometryType type) : this(OgrPINVOKE.new_Geometry((int)type, IntPtr.Zero, 0, IntPtr.Zero, IntPtr.Zero), true, null) {
7575
if (OgrPINVOKE.SWIGPendingException.Pending) throw OgrPINVOKE.SWIGPendingException.Retrieve();
7676
}
7777
}

swig/include/csharp/typemaps_csharp.i

Lines changed: 2 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
%include "typemaps.i"
1616
%include "arrays_csharp.i"
17+
%include "csharp_strings.i"
1718

1819
/* CSHARP TYPEMAPS */
1920

@@ -87,17 +88,6 @@ OGRErrMessages( int rc ) {
8788
*/
8889

8990
%pragma(csharp) modulecode=%{
90-
internal static byte[] StringToUtf8Bytes(string str)
91-
{
92-
if (str == null)
93-
return null;
94-
95-
int bytecount = System.Text.Encoding.UTF8.GetMaxByteCount(str.Length);
96-
byte[] bytes = new byte[bytecount + 1];
97-
System.Text.Encoding.UTF8.GetBytes(str, 0, str.Length, bytes, 0);
98-
return bytes;
99-
}
100-
10191
internal unsafe static string Utf8BytesToString(IntPtr pNativeData)
10292
{
10393
if (pNativeData == IntPtr.Zero)
@@ -221,7 +211,7 @@ CSHARP_OBJECT_ARRAYS_PINNED(GDALRasterBandShadow, Band)
221211
public StringListMarshal(string[] ar) {
222212
_ar = new IntPtr[ar.Length+1];
223213
for (int cx = 0; cx < ar.Length; cx++) {
224-
_ar[cx] = StringToUtf8Unmanaged(ar[cx]);
214+
_ar[cx] = $modulePINVOKE.StringToUtf8Unmanaged(ar[cx]);
225215
}
226216
_ar[ar.Length] = IntPtr.Zero;
227217
}
@@ -231,27 +221,6 @@ CSHARP_OBJECT_ARRAYS_PINNED(GDALRasterBandShadow, Band)
231221
}
232222
GC.SuppressFinalize(this);
233223
}
234-
235-
static IntPtr StringToUtf8Unmanaged(string str) {
236-
if (str == null)
237-
return IntPtr.Zero;
238-
239-
int byteCount = System.Text.Encoding.UTF8.GetByteCount(str);
240-
IntPtr unmanagedString = Marshal.AllocHGlobal(byteCount + 1);
241-
242-
unsafe
243-
{
244-
byte* ptr = (byte*)unmanagedString.ToPointer();
245-
fixed (char *pStr = str)
246-
{
247-
System.Text.Encoding.UTF8.GetBytes(pStr, str.Length, ptr, byteCount);
248-
// null-terminate
249-
ptr[byteCount] = 0;
250-
}
251-
}
252-
253-
return unmanagedString;
254-
}
255224
}
256225
%}
257226

@@ -476,24 +445,6 @@ CSHARP_OBJECT_ARRAYS_PINNED(GDALRasterBandShadow, Band)
476445

477446
%apply (int inout[ANY]) {int *pList};
478447

479-
/*
480-
* Typemap for const char *utf8_string
481-
*/
482-
%typemap(csin) (const char *utf8_string) "$module.StringToUtf8Bytes($csinput)"
483-
%typemap(imtype, out="IntPtr") (const char *utf8_string) "byte[]"
484-
%typemap(out) (const char *utf8_string) %{ $result = $1; %}
485-
%typemap(csout, excode=SWIGEXCODE) (const char *utf8_string) {
486-
/* %typemap(csout) (const char *utf8_string) */
487-
IntPtr cPtr = $imcall;
488-
string ret = $module.Utf8BytesToString(cPtr);
489-
$excode
490-
return ret;
491-
}
492-
493-
%apply ( const char *utf8_string ) {
494-
const char* GetFieldAsString
495-
};
496-
497448
/*
498449
* Typemap for double *defaultval.
499450
*/

swig/include/gdalconst.i

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
#if defined(SWIGCSHARP)
1919
%module GdalConst
20+
%include csharp_strings.i
2021
#else
2122
%module gdalconst
2223
#endif
@@ -291,6 +292,12 @@
291292
// without the prefix.
292293
// TODO GDAL 4.0: clean this up!
293294

295+
#if defined(SWIGCSHARP)
296+
%csconst(1);
297+
#elif defined(SWIGJAVA)
298+
%javaconst(1);
299+
#endif
300+
294301
#define DMD_LONGNAME "DMD_LONGNAME"
295302
#define GDAL_DMD_LONGNAME "DMD_LONGNAME"
296303
#define DMD_HELPTOPIC "DMD_HELPTOPIC"
@@ -455,6 +462,12 @@
455462
#define GDsCFastGetExtent "FastGetExtent"
456463
#define GDsCFastGetExtentWGS84LongLat "FastGetExtentWGS84LongLat"
457464

465+
#if defined(SWIGCSHARP)
466+
%csconst(0);
467+
#elif defined(SWIGJAVA)
468+
%javaconst(0);
469+
#endif
470+
458471
#endif
459472

460473
%constant CPLES_BackslashQuotable = CPLES_BackslashQuotable;

0 commit comments

Comments
 (0)