csharp.py 12 KB

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