comparison mupdf-source/scripts/wrap/csharp.py @ 2:b50eed0cc0ef upstream

ADD: MuPDF v1.26.7: the MuPDF source as downloaded by a default build of PyMuPDF 1.26.4. The directory name has changed: no version number in the expanded directory now.
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 15 Sep 2025 11:43:07 +0200
parents
children
comparison
equal deleted inserted replaced
1:1d09e1dec1d9 2:b50eed0cc0ef
1 '''
2 Things for generating C#-specific output.
3 '''
4 from . import cpp
5 from . import parse
6 from . import rename
7 from . import state
8 from . import util
9
10 import jlib
11
12 import textwrap
13 import os
14
15
16 def make_outparam_helper_csharp(
17 tu,
18 cursor,
19 fnname,
20 fnname_wrapper,
21 generated,
22 main_name,
23 ):
24 '''
25 Write C# code for a convenient tuple-returning wrapper for MuPDF
26 function that has out-params. We use the C# wrapper for our generated
27 {main_name}_outparams() function.
28
29 We don't attempt to handle functions that take unsigned char* args
30 because these generally indicate sized binary data and cannot be handled
31 generically.
32 '''
33 def write(text):
34 generated.swig_csharp.write(text)
35
36 main_name = rename.ll_fn(cursor.mangled_name)
37 return_void = cursor.result_type.spelling == 'void'
38 if fnname == 'fz_buffer_extract':
39 # Write custom wrapper that returns the binary data as a C# bytes
40 # array, using the C# wrapper for buffer_extract_outparams_fn(fz_buffer
41 # buf, buffer_extract_outparams outparams).
42 #
43 write(
44 textwrap.dedent(
45 f'''
46
47 // Custom C# wrapper for fz_buffer_extract().
48 public static class mupdf_{rename.class_('fz_buffer')}_extract
49 {{
50 public static byte[] {rename.method('fz_buffer', 'fz_buffer_extract')}(this mupdf.{rename.class_('fz_buffer')} buffer)
51 {{
52 var outparams = new mupdf.{rename.ll_fn('fz_buffer_storage')}_outparams();
53 uint n = mupdf.mupdf.{rename.ll_fn('fz_buffer_storage')}_outparams_fn(buffer.m_internal, outparams);
54 var raw1 = mupdf.SWIGTYPE_p_unsigned_char.getCPtr(outparams.datap);
55 System.IntPtr raw2 = System.Runtime.InteropServices.HandleRef.ToIntPtr(raw1);
56 byte[] ret = new byte[n];
57 // Marshal.Copy() raises exception if <raw2> is null even if <n> is zero.
58 if (n == 0) return ret;
59 System.Runtime.InteropServices.Marshal.Copy(raw2, ret, 0, (int) n);
60 buffer.{rename.method( 'fz_buffer', 'fz_clear_buffer')}();
61 buffer.{rename.method( 'fz_buffer', 'fz_trim_buffer')}();
62 return ret;
63 }}
64 }}
65 ''')
66 )
67 return
68
69 # We don't attempt to generate wrappers for fns that take or return
70 # 'unsigned char*' - swig does not treat these as zero-terminated strings,
71 # and they are generally binary data so cannot be handled generically.
72 #
73 if parse.is_pointer_to(cursor.result_type, 'unsigned char'):
74 jlib.log(f'Cannot generate C# out-param wrapper for {cursor.mangled_name} because it returns unsigned char*.', 1)
75 return
76 for arg in parse.get_args( tu, cursor):
77 if parse.is_pointer_to(arg.cursor.type, 'unsigned char'):
78 jlib.log(f'Cannot generate C# out-param wrapper for {cursor.mangled_name} because has unsigned char* arg.', 1)
79 return
80 if parse.is_pointer_to_pointer_to(arg.cursor.type, 'unsigned char'):
81 jlib.log(f'Cannot generate C# out-param wrapper for {cursor.mangled_name} because has unsigned char** arg.', 1)
82 return
83 if arg.cursor.type.get_array_size() >= 0:
84 jlib.log(f'Cannot generate C# out-param wrapper for {cursor.mangled_name} because has array arg.', 1)
85 return
86 if arg.cursor.type.kind == state.clang.cindex.TypeKind.POINTER:
87 pointee = state.get_name_canonical( arg.cursor.type.get_pointee())
88 if pointee.kind == state.clang.cindex.TypeKind.ENUM:
89 jlib.log(f'Cannot generate C# out-param wrapper for {cursor.mangled_name} because has enum out-param arg.', 1)
90 return
91 if pointee.kind == state.clang.cindex.TypeKind.FUNCTIONPROTO:
92 jlib.log(f'Cannot generate C# out-param wrapper for {cursor.mangled_name} because has fn-ptr arg.', 1)
93 return
94 if pointee.is_const_qualified():
95 # Not an out-param.
96 jlib.log(f'Cannot generate C# out-param wrapper for {cursor.mangled_name} because has pointer-to-const arg.', 1)
97 return
98 if arg.cursor.type.get_pointee().spelling == 'FILE':
99 jlib.log(f'Cannot generate C# out-param wrapper for {cursor.mangled_name} because has FILE* arg.', 1)
100 return
101 if pointee.spelling == 'void':
102 jlib.log(f'Cannot generate C# out-param wrapper for {cursor.mangled_name} because has void* arg.', 1)
103 return
104
105 num_return_values = 0 if return_void else 1
106 for arg in parse.get_args( tu, cursor):
107 if arg.out_param:
108 num_return_values += 1
109 assert num_return_values
110
111 if num_return_values > 7:
112 # On linux, mono-csc can fail with:
113 # System.NotImplementedException: tuples > 7
114 #
115 jlib.log(f'Cannot generate C# out-param wrapper for {cursor.mangled_name} because would require > 7-tuple.')
116 return
117
118 # Write C# wrapper.
119 arg0, _ = parse.get_first_arg( tu, cursor)
120 if not arg0.alt:
121 return
122
123 method_name = rename.method( arg0.alt.type.spelling, fnname)
124
125 write(f'\n')
126 write(f'// Out-params extension method for C# class {rename.class_(arg0.alt.type.spelling)} (wrapper for MuPDF struct {arg0.alt.type.spelling}),\n')
127 write(f'// adding class method {method_name}() (wrapper for {fnname}())\n')
128 write(f'// which returns out-params directly.\n')
129 write(f'//\n')
130 write(f'public static class mupdf_{main_name}_outparams_helper\n')
131 write(f'{{\n')
132 write(f' public static ')
133
134 def write_type(alt, type_):
135 if alt:
136 write(f'mupdf.{rename.class_(alt.type.spelling)}')
137 elif parse.is_pointer_to(type_, 'char'):
138 write( f'string')
139 else:
140 text = cpp.declaration_text(type_, '').strip()
141 if text == 'int16_t': text = 'short'
142 elif text == 'int64_t': text = 'long'
143 elif text == 'size_t': text = 'ulong'
144 elif text == 'unsigned int': text = 'uint'
145 elif text.startswith('enum '):
146 # This is primarily for enum pdf_zugferd_profile; C# does not
147 # like `enum` prefix, and we need to specify namespace name
148 # `mupdf`.
149 text = text[5:]
150 if text.startswith('pdf_') or text.startswith('fz_'):
151 text = f'{rename.namespace()}.{text}'
152 write(f'{text}')
153
154 # Generate the returned tuple.
155 #
156 if num_return_values > 1:
157 write('(')
158
159 sep = ''
160
161 # Returned param, if any.
162 if not return_void:
163 return_alt = None
164 base_type_cursor, base_typename, extras = parse.get_extras( tu, cursor.result_type)
165 if extras:
166 if extras.opaque:
167 # E.g. we don't have access to definition of fz_separation,
168 # but it is marked in classextras with opaque=true, so
169 # there will be a wrapper class.
170 return_alt = base_type_cursor
171 elif base_type_cursor.kind == state.clang.cindex.CursorKind.STRUCT_DECL:
172 return_alt = base_type_cursor
173 write_type(return_alt, cursor.result_type)
174 sep = ', '
175
176 # Out-params.
177 for arg in parse.get_args( tu, cursor):
178 if arg.out_param:
179 write(sep)
180 write_type(arg.alt, arg.cursor.type.get_pointee())
181 if num_return_values > 1:
182 write(f' {arg.name_csharp}')
183 sep = ', '
184
185 if num_return_values > 1:
186 write(')')
187
188 # Generate function name and params. If first arg is a wrapper class we
189 # use C#'s 'this' keyword to make this a member function of the wrapper
190 # class.
191 #jlib.log('outputs fn {fnname=}: is member: {"yes" if arg0.alt else "no"}')
192 write(f' ')
193 write( method_name if arg0.alt else 'fn')
194 write(f'(')
195 if arg0.alt: write('this ')
196 sep = ''
197 for arg in parse.get_args( tu, cursor):
198 if arg.out_param:
199 continue
200 write(sep)
201 if arg.alt:
202 # E.g. 'Document doc'.
203 write(f'mupdf.{rename.class_(arg.alt.type.spelling)} {arg.name_csharp}')
204 elif parse.is_pointer_to(arg.cursor.type, 'char'):
205 write(f'string {arg.name_csharp}')
206 else:
207 text = cpp.declaration_text(arg.cursor.type, arg.name_csharp).strip()
208 text = util.clip(text, 'const ')
209 text = text.replace('int16_t ', 'short ')
210 text = text.replace('int64_t ', 'long ')
211 text = text.replace('size_t ', 'uint ')
212 text = text.replace('unsigned int ', 'uint ')
213 write(text)
214 sep = ', '
215 write(f')\n')
216
217 # Function body.
218 #
219 write(f' {{\n')
220
221 # Create local outparams struct.
222 write(f' var outparams = new mupdf.{main_name}_outparams();\n')
223 write(f' ')
224
225 # Generate function call.
226 #
227 # The C# *_outparams_fn() generated by swig is inside namespace mupdf {
228 # class mupdf { ... } }, so we access it using the rather clumsy prefix
229 # 'mupdf.mupdf.'. It will have been generated from a C++ function
230 # (generate by us) which is in top-level namespace mupdf, but swig
231 # appears to generate the same code even if the C++ function is not in
232 # a namespace.
233 #
234 if not return_void:
235 write(f'var ret = ')
236 write(f'mupdf.mupdf.{main_name}_outparams_fn(')
237 sep = ''
238 for arg in parse.get_args( tu, cursor):
239 if arg.out_param:
240 continue
241 write(f'{sep}{arg.name_csharp}')
242 if arg.alt:
243 extras = parse.get_fz_extras( tu, arg.alt.type.spelling)
244 assert extras.pod != 'none' \
245 'Cannot pass wrapper for {type_.spelling} as arg because pod is "none" so we cannot recover struct.'
246 write('.internal_()' if extras.pod == 'inline' else '.m_internal')
247 sep = ', '
248 write(f'{sep}outparams);\n')
249
250 # Generate return of tuple.
251 write(f' return ')
252 if num_return_values > 1:
253 write(f'(')
254 sep = ''
255 if not return_void:
256 if return_alt:
257 write(f'new mupdf.{rename.class_(return_alt.type.spelling)}(ret)')
258 else:
259 write(f'ret')
260 sep = ', '
261 for arg in parse.get_args( tu, cursor):
262 if arg.out_param:
263 write(f'{sep}')
264 type_ = arg.cursor.type.get_pointee()
265 if arg.alt:
266 write(f'new mupdf.{rename.class_(arg.alt.type.spelling)}(outparams.{arg.name_csharp})')
267 elif 0 and parse.is_pointer_to(type_, 'char'):
268 # This was intended to convert char* to string, but swig
269 # will have already done that when making a C# version of
270 # the C++ struct, and modern csc on Windows doesn't like
271 # creating a string from a string for some reason.
272 write(f'new string(outparams.{arg.name_csharp})')
273 else:
274 write(f'outparams.{arg.name_csharp}')
275 sep = ', '
276 if num_return_values > 1:
277 write(')')
278 write(';\n')
279 write(f' }}\n')
280 write(f'}}\n')
281
282
283 def csharp_settings(build_dirs):
284 '''
285 Returns (csc, mono, mupdf_cs).
286
287 csc: C# compiler.
288 mono: C# interpreter ("" on Windows).
289 mupdf_cs: MuPDF C# code.
290
291 `mupdf_cs` will be None if `build_dirs` is false.
292
293 E.g. on Windows `csc` can be: C:/Program Files (x86)/Microsoft Visual Studio/2019/Community/MSBuild/Current/Bin/Roslyn/csc.exe
294 '''
295 # On linux requires:
296 # sudo apt install mono-devel
297 #
298 # OpenBSD:
299 # pkg_add mono
300 # but we get runtime error when exiting:
301 # mono:build/shared-release/libmupdfcpp.so: undefined symbol '_ZdlPv'
302 # which might be because of mixing gcc and clang?
303 #
304 if state.state_.windows:
305 import wdev
306 vs = wdev.WindowsVS()
307 jlib.log('{vs.description_ml()=}')
308 csc = vs.csc
309 jlib.log('{csc=}')
310 assert csc, f'Unable to find csc.exe'
311 mono = ''
312 else:
313 mono = 'mono'
314 if state.state_.linux:
315 csc = 'mono-csc'
316 elif state.state_.openbsd:
317 csc = 'csc'
318 else:
319 assert 0, f'Do not know where to find mono. {platform.platform()=}'
320
321 if build_dirs:
322 mupdf_cs = os.path.relpath(f'{build_dirs.dir_so}/mupdf.cs')
323 else:
324 mupdf_cs = None
325 return csc, mono, mupdf_cs