@@ -235,54 +235,81 @@ private unsafe TypstCompiler(string? inputPath, string? inputSource, Fonts? font
235235 root = Path . GetDirectoryName ( inputPath ) ;
236236 }
237237
238- var inputPathPtr = inputPath != null ? Marshal . StringToCoTaskMemUTF8 ( inputPath ) : IntPtr . Zero ;
239-
240- // The source goes over as raw UTF-8 bytes with an explicit length. A Typst
241- // document may contain NUL bytes, and a NUL-terminated string would be
242- // silently truncated at the first one.
243- byte [ ] ? inputSourceBytes = null ;
238+ // Every one of these is native memory that the finally block below releases, so they are
239+ // declared out here and allocated inside the try. Allocating them before it would leak
240+ // whatever had been allocated already if a later step threw, and several of them can:
241+ // fontPaths may be a lazy sequence supplied by the caller, and sysInputs is serialized.
242+ IntPtr inputPathPtr = IntPtr . Zero ;
243+ IntPtr inputSourcePtr = IntPtr . Zero ;
244244 nuint inputSourceLen = 0 ;
245- if ( inputSource != null )
246- {
247- var encoded = Encoding . UTF8 . GetBytes ( inputSource ) ;
248- inputSourceLen = ( nuint ) encoded . Length ;
249- // `fixed` over an empty array yields a null pointer, which the native
250- // side reads as "no source at all". A one-byte placeholder keeps an
251- // empty document distinguishable; the length passed stays 0.
252- inputSourceBytes = encoded . Length == 0 ? new byte [ 1 ] : encoded ;
253- }
254-
255245 IntPtr rootPtr = IntPtr . Zero ;
256- if ( ! string . IsNullOrWhiteSpace ( root ) )
257- {
258- rootPtr = Marshal . StringToCoTaskMemUTF8 ( root ) ;
259- }
246+ IntPtr [ ] fontPathPtrs = [ ] ;
247+ int fontPathCount = 0 ;
248+ IntPtr packagePathPtr = IntPtr . Zero ;
249+ IntPtr sysInputsPtr = IntPtr . Zero ;
260250
261- var fontPathsList = fontPaths . ToList ( ) ;
262- var fontPathPtrs = new IntPtr [ fontPathsList . Count ] ;
263- for ( int i = 0 ; i < fontPathsList . Count ; i ++ )
251+ try
264252 {
265- fontPathPtrs [ i ] = Marshal . StringToCoTaskMemUTF8 ( fontPathsList [ i ] ) ;
266- }
253+ if ( inputPath != null )
254+ {
255+ inputPathPtr = Marshal . StringToCoTaskMemUTF8 ( inputPath ) ;
256+ }
267257
268- var packagePathPtr = packagePath != null ? Marshal . StringToCoTaskMemUTF8 ( packagePath ) : IntPtr . Zero ;
258+ // The source goes over as raw UTF-8 bytes with an explicit length. A Typst
259+ // document may contain NUL bytes, and a NUL-terminated string would be
260+ // silently truncated at the first one.
261+ if ( inputSource != null )
262+ {
263+ // Encoding straight into native memory keeps a document-sized array off the managed
264+ // heap; a source of any size would otherwise be copied there, and a large one would
265+ // land on the large object heap, only to be garbage as soon as the call returns.
266+ int byteCount = Encoding . UTF8 . GetByteCount ( inputSource ) ;
267+
268+ // A null pointer reads as "no source at all" on the native side, so an empty
269+ // document still needs one real byte behind the pointer; the length stays 0.
270+ inputSourcePtr = Marshal . AllocCoTaskMem ( byteCount == 0 ? 1 : byteCount ) ;
271+ if ( byteCount > 0 )
272+ {
273+ fixed ( char * chars = inputSource )
274+ {
275+ Encoding . UTF8 . GetBytes ( chars , inputSource . Length , ( byte * ) inputSourcePtr , byteCount ) ;
276+ }
277+ }
269278
270- var sysInputsJson = sysInputs == null ? "{}" : JsonSerializer . Serialize < Dictionary < string , string > > ( sysInputs , sourceGenOptions ) ;
271- var sysInputsPtr = Marshal . StringToCoTaskMemUTF8 ( sysInputsJson ) ;
279+ inputSourceLen = ( nuint ) byteCount ;
280+ }
281+
282+ if ( ! string . IsNullOrWhiteSpace ( root ) )
283+ {
284+ rootPtr = Marshal . StringToCoTaskMemUTF8 ( root ) ;
285+ }
286+
287+ var fontPathsList = fontPaths . ToList ( ) ;
288+ fontPathCount = fontPathsList . Count ;
289+ fontPathPtrs = new IntPtr [ fontPathCount ] ;
290+ for ( int i = 0 ; i < fontPathCount ; i ++ )
291+ {
292+ fontPathPtrs [ i ] = Marshal . StringToCoTaskMemUTF8 ( fontPathsList [ i ] ) ;
293+ }
294+
295+ if ( packagePath != null )
296+ {
297+ packagePathPtr = Marshal . StringToCoTaskMemUTF8 ( packagePath ) ;
298+ }
299+
300+ var sysInputsJson = sysInputs == null ? "{}" : JsonSerializer . Serialize < Dictionary < string , string > > ( sysInputs , sourceGenOptions ) ;
301+ sysInputsPtr = Marshal . StringToCoTaskMemUTF8 ( sysInputsJson ) ;
272302
273- try
274- {
275303 fixed ( IntPtr * fontPathsRawPtr = fontPathPtrs )
276- fixed ( byte * inputSourcePtr = inputSourceBytes )
277304 {
278- IntPtr * fontPathsPtr = fontPathsList . Count == 0 ? null : fontPathsRawPtr ;
305+ IntPtr * fontPathsPtr = fontPathCount == 0 ? null : fontPathsRawPtr ;
279306 _compiler = CsBindgen . NativeMethods . create_compiler (
280307 ( byte * ) rootPtr ,
281308 ( byte * ) inputPathPtr ,
282- inputSourcePtr ,
309+ ( byte * ) inputSourcePtr ,
283310 inputSourceLen ,
284311 ( byte * * ) fontPathsPtr ,
285- ( nuint ) fontPathsList . Count ,
312+ ( nuint ) fontPathCount ,
286313 ( byte * ) packagePathPtr ,
287314 ( byte * ) sysInputsPtr ,
288315 ignoreSystemFonts ,
@@ -296,11 +323,14 @@ private unsafe TypstCompiler(string? inputPath, string? inputSource, Fonts? font
296323 }
297324 finally
298325 {
326+ // FreeCoTaskMem ignores a null pointer, so the entries of a partly filled font path
327+ // array need no guard of their own.
299328 if ( rootPtr != IntPtr . Zero ) Marshal . FreeCoTaskMem ( rootPtr ) ;
300329 if ( inputPathPtr != IntPtr . Zero ) Marshal . FreeCoTaskMem ( inputPathPtr ) ;
330+ if ( inputSourcePtr != IntPtr . Zero ) Marshal . FreeCoTaskMem ( inputSourcePtr ) ;
301331 foreach ( var ptr in fontPathPtrs ) Marshal . FreeCoTaskMem ( ptr ) ;
302332 if ( packagePathPtr != IntPtr . Zero ) Marshal . FreeCoTaskMem ( packagePathPtr ) ;
303- Marshal . FreeCoTaskMem ( sysInputsPtr ) ;
333+ if ( sysInputsPtr != IntPtr . Zero ) Marshal . FreeCoTaskMem ( sysInputsPtr ) ;
304334 }
305335 }
306336
0 commit comments