Skip to content

Generators API

Generators convert the intermediate representation (IR) into target language code. Each generator is responsible for producing idiomatic code in its target language.

Overview

The generator architecture allows extending Polyglot FFI to new target languages without modifying the parser or IR. Each generator implements the same interface but produces different output.

Ctypes Generator

Generates OCaml ctypes type and function descriptions.

polyglot_ffi.generators.ctypes_gen.CtypesGenerator

Generate OCaml ctypes binding code.

Source code in src/polyglot_ffi/generators/ctypes_gen.py
class CtypesGenerator:
    """Generate OCaml ctypes binding code."""

    # Map IR primitive types to Ctypes types
    TYPE_MAP: Dict[str, str] = {
        "string": "string",
        "int": "int",
        "float": "double",
        "bool": "bool",
        "unit": "void",
    }

    def generate_type_description(self, module: IRModule) -> str:
        """
        Generate type_description.ml.

        Basic boilerplate only (no complex types).
        """
        return """(* Generated by polyglot-ffi *)

module Types (F : Ctypes.TYPE) = struct
  (* Type descriptions go here if needed for complex types *)
  (* Foundational types: Only primitives, no custom types needed *)
end
"""

    def generate_function_description(self, module: IRModule) -> str:
        """
        Generate function_description.ml with foreign function declarations.
        """
        lines = [
            "(* Generated by polyglot-ffi *)",
            "open Ctypes",
            "",
            "module Functions (F : Ctypes.FOREIGN) = struct",
            "  open F",
        ]

        for func in module.functions:
            # Generate foreign function declaration
            lines.append(f"  let {func.name} =")
            lines.append(f'    F.foreign "ml_{func.name}"')

            # Build the ctypes signature
            sig_parts = []

            # Add parameter types
            for param in func.params:
                ctype = self._get_ctype(param.type)
                sig_parts.append(ctype)
                # Add length parameter for list types
                if param.type.kind == TypeKind.LIST:
                    sig_parts.append("int")

            # Add return type
            return_ctype = self._get_ctype(func.return_type)

            # Construct the signature
            sig_line = "      ("
            if sig_parts:
                sig_line += " @-> ".join(sig_parts) + " @-> "
            sig_line += f"returning {return_ctype})"

            lines.append(sig_line)
            lines.append("")

        lines.append("end")

        return "\n".join(lines)

    def _get_ctype(self, ir_type: IRType) -> str:
        """
        Convert IR type to Ctypes type string.

        Handles:
        - Primitives: string, int, float, bool, unit
        - Options: mapped to OCaml option using ptr (nullable)
        - Lists: mapped to OCaml list (opaque for C FFI)
        - Tuples: serialized as composite values
        - Custom types: referenced by name
        """
        if ir_type.is_primitive():
            return self.TYPE_MAP.get(ir_type.name, "string")

        # Handle option types
        # Options in OCaml FFI: For C interop, we typically pass options as pointers
        # None = NULL, Some x = pointer to x
        if ir_type.kind == TypeKind.OPTION:
            if ir_type.params:
                inner_type = ir_type.params[0]
                # Special case: string is already a pointer (char*), so string option
                # should just be string (nullable), not ptr string (char**)
                if inner_type.name == "string":
                    return "string"
                # For other primitives, wrap in ptr to make them nullable
                inner_ctype = self._get_ctype(inner_type)
                # Wrap in parentheses to ensure correct parsing in ctypes expressions
                return f"(ptr {inner_ctype})"
            return "(ptr void)"

        # Handle list types
        # Lists in OCaml FFI: Lists are OCaml values, passed as abstract pointers
        if ir_type.kind == TypeKind.LIST:
            # Next features, we'll represent lists as opaque OCaml values
            # Full marshaling support comes in future versions
            # Wrap in parentheses to ensure correct parsing in ctypes expressions
            return "(ptr void)"  # Opaque list pointer

        # Handle tuple types
        # Tuples in OCaml FFI: Can be passed as structures or individual params
        if ir_type.kind == TypeKind.TUPLE:
            # next features, represent tuples as opaque values
            # Full struct support comes in future versions
            # Wrap in parentheses to ensure correct parsing in ctypes expressions
            return "(ptr void)"  # Opaque tuple pointer

        # Handle custom types (records, variants, type aliases)
        if ir_type.kind in (TypeKind.CUSTOM, TypeKind.RECORD, TypeKind.VARIANT):
            # Custom types are passed as opaque pointers in C FFI
            # Wrap in parentheses to ensure correct parsing in ctypes expressions
            return "(ptr void)"  # Opaque custom type pointer

        raise ValueError(f"Unsupported type for Ctypes generation: {ir_type}")

Functions

generate_type_description(module)

Generate type_description.ml.

Basic boilerplate only (no complex types).

Source code in src/polyglot_ffi/generators/ctypes_gen.py
    def generate_type_description(self, module: IRModule) -> str:
        """
        Generate type_description.ml.

        Basic boilerplate only (no complex types).
        """
        return """(* Generated by polyglot-ffi *)

module Types (F : Ctypes.TYPE) = struct
  (* Type descriptions go here if needed for complex types *)
  (* Foundational types: Only primitives, no custom types needed *)
end
"""
generate_function_description(module)

Generate function_description.ml with foreign function declarations.

Source code in src/polyglot_ffi/generators/ctypes_gen.py
def generate_function_description(self, module: IRModule) -> str:
    """
    Generate function_description.ml with foreign function declarations.
    """
    lines = [
        "(* Generated by polyglot-ffi *)",
        "open Ctypes",
        "",
        "module Functions (F : Ctypes.FOREIGN) = struct",
        "  open F",
    ]

    for func in module.functions:
        # Generate foreign function declaration
        lines.append(f"  let {func.name} =")
        lines.append(f'    F.foreign "ml_{func.name}"')

        # Build the ctypes signature
        sig_parts = []

        # Add parameter types
        for param in func.params:
            ctype = self._get_ctype(param.type)
            sig_parts.append(ctype)
            # Add length parameter for list types
            if param.type.kind == TypeKind.LIST:
                sig_parts.append("int")

        # Add return type
        return_ctype = self._get_ctype(func.return_type)

        # Construct the signature
        sig_line = "      ("
        if sig_parts:
            sig_line += " @-> ".join(sig_parts) + " @-> "
        sig_line += f"returning {return_ctype})"

        lines.append(sig_line)
        lines.append("")

    lines.append("end")

    return "\n".join(lines)

Usage Example

from polyglot_ffi.parsers.ocaml import parse_mli_file
from polyglot_ffi.generators.ctypes_gen import CtypesGenerator

# Parse OCaml interface
module = parse_mli_file(Path("crypto.mli"))

# Generate ctypes bindings
generator = CtypesGenerator()
type_desc = generator.generate_type_description(module)
func_desc = generator.generate_function_description(module)

# Write to files
Path("type_description.ml").write_text(type_desc)
Path("function_description.ml").write_text(func_desc)

Generated Output

For val encrypt : string -> string:

(* type_description.ml *)
open Ctypes

let string = Ctypes.string

(* function_description.ml *)
open Ctypes

let encrypt = foreign "ml_encrypt" (string @-> returning string)

C Stubs Generator

Generates memory-safe C wrapper code with proper CAMLparam/CAMLreturn macros.

polyglot_ffi.generators.c_stubs_gen.CStubGenerator

Generate C wrapper code for OCaml functions.

Source code in src/polyglot_ffi/generators/c_stubs_gen.py
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
class CStubGenerator:
    """Generate C wrapper code for OCaml functions."""

    # Map IR types to C types
    C_TYPE_MAP: Dict[str, str] = {
        "string": "const char*",
        "int": "int",
        "float": "double",
        "bool": "int",
        "unit": "void",
    }

    def generate_stubs(self, module: IRModule, module_name: str) -> str:
        """Generate C stub implementation file."""
        safe_name = sanitize_module_name(module_name)
        lines = [
            f"/* Generated by polyglot-ffi */",
            f"/* {safe_name}_stubs.c */",
            "",
            "#include <string.h>",
            "#include <stdlib.h>",
            "#include <stdint.h>",
            "#include <caml/mlvalues.h>",
            "#include <caml/memory.h>",
            "#include <caml/alloc.h>",
            "#include <caml/callback.h>",
            "",
            "/* OCaml runtime initialization - call once before using any functions */",
            "static int _ocaml_initialized = 0;",
            "",
            "void ml_init(void) {",
            "    if (!_ocaml_initialized) {",
            "        char* argv[] = {NULL};",
            "        caml_startup(argv);",
            "        _ocaml_initialized = 1;",
            "    }",
            "}",
            "",
            "/* Memory cleanup functions */",
            "",
            "/* Free option type results (int*, double*, etc.) */",
            "void ml_free_option(void* ptr) {",
            "    if (ptr) {",
            "        free(ptr);",
            "    }",
            "}",
            "",
            "/* Free list results returned by ml_* functions */",
            "/* For primitive lists (int, float, bool), just frees the structure */",
            "void ml_free_list_result(void* result) {",
            "    if (result) {",
            "        void** res_array = (void**)result;",
            "        if (res_array[1]) {",
            "            free(res_array[1]);  // Free array",
            "        }",
            "        free(result);  // Free result struct",
            "    }",
            "}",
            "",
            "/* For string lists, frees each string and the structure */",
            "void ml_free_string_list_result(void* result) {",
            "    if (result) {",
            "        void** res_array = (void**)result;",
            "        int len = (int)(intptr_t)res_array[0];",
            "        if (res_array[1]) {",
            "            const char** str_array = (const char**)res_array[1];",
            "            for (int i = 0; i < len; i++) {",
            "                if (str_array[i]) {",
            "                    free((void*)str_array[i]);  // Free each string",
            "                }",
            "            }",
            "            free(str_array);  // Free array",
            "        }",
            "        free(result);  // Free result struct",
            "    }",
            "}",
            "",
            "/* For tuple lists, frees each tuple and the structure */",
            "void ml_free_tuple_list_result(void* result) {",
            "    if (result) {",
            "        void** res_array = (void**)result;",
            "        int len = (int)(intptr_t)res_array[0];",
            "        if (res_array[1]) {",
            "            void** tuple_array = (void**)res_array[1];",
            "            for (int i = 0; i < len; i++) {",
            "                if (tuple_array[i]) {",
            "                    free(tuple_array[i]);  // Free each tuple",
            "                }",
            "            }",
            "            free(tuple_array);  // Free array",
            "        }",
            "        free(result);  // Free result struct",
            "    }",
            "}",
            "",
        ]

        for func in module.functions:
            lines.extend(self._generate_function_stub(func))
            lines.append("")

        return "\n".join(lines)

    def generate_header(self, module: IRModule, module_name: str) -> str:
        """Generate C header file."""
        safe_name = sanitize_module_name(module_name)
        guard = f"{safe_name.upper()}_STUBS_H"

        lines = [
            f"/* Generated by polyglot-ffi */",
            f"/* {safe_name}_stubs.h */",
            "",
            f"#ifndef {guard}",
            f"#define {guard}",
            "",
            "/* Initialize OCaml runtime - must be called before any other functions */",
            "void ml_init(void);",
            "",
        ]

        # Function declarations
        for func in module.functions:
            c_return = self._get_c_type(func.return_type)
            # Filter out unit parameters - they should not appear in C signatures
            non_unit_params = [p for p in func.params if p.type.name != "unit"]
            if non_unit_params:
                param_parts = []
                for p in non_unit_params:
                    param_parts.append(f"{self._get_c_type(p.type)} {p.name}")
                    # For list types, add a length parameter
                    if p.type.kind == TypeKind.LIST:
                        param_parts.append(f"int {p.name}_len")
                params = ", ".join(param_parts)
            else:
                params = "void"
            lines.append(f"{c_return} ml_{func.name}({params});")

        lines.append("")
        lines.append("/* Memory cleanup functions */")
        lines.append("/* NOTE: Caller must free returned pointers for option and list types */")
        lines.append("")
        lines.append("/* Free option type results (int*, double*, etc.) */")
        lines.append("void ml_free_option(void* ptr);")
        lines.append("")
        lines.append("/* Free list results returned by ml_* functions */")
        lines.append("/* For primitive lists (int, float, bool), just frees the structure */")
        lines.append("void ml_free_list_result(void* result);")
        lines.append("")
        lines.append("/* For string lists, frees each string and the structure */")
        lines.append("void ml_free_string_list_result(void* result);")
        lines.append("")
        lines.append("/* For tuple lists, frees each tuple and the structure */")
        lines.append("void ml_free_tuple_list_result(void* result);")
        lines.append("")
        lines.append(f"#endif /* {guard} */")

        return "\n".join(lines)

    def _generate_function_stub(self, func: IRFunction) -> list:
        """Generate C stub for a single function."""
        lines = []

        # Function signature
        c_return = self._get_c_type(func.return_type)
        # Filter out unit parameters - they should not appear in C signatures
        non_unit_params = [p for p in func.params if p.type.name != "unit"]
        if non_unit_params:
            param_parts = []
            for p in non_unit_params:
                param_parts.append(f"{self._get_c_type(p.type)} {p.name}")
                # For list types, add a length parameter
                if p.type.kind == TypeKind.LIST:
                    param_parts.append(f"int {p.name}_len")
            params = ", ".join(param_parts)
        else:
            params = "void"

        lines.append(f"/* Wrapper for OCaml {func.name} function */")
        lines.append(f"{c_return} ml_{func.name}({params}) {{")
        lines.append("    CAMLparam0();")

        # Declare local variables (only for non-unit parameters)
        # Note: we still need ml_result
        for param in non_unit_params:
            lines.append(f"    CAMLlocal1(ml_{param.name});")
        lines.append("    CAMLlocal1(ml_result);")
        lines.append("")

        # Convert C parameters to OCaml values (skip unit parameters)
        for param in non_unit_params:
            lines.extend(self._convert_c_to_ocaml(param.name, param.type))

        # Call OCaml function - use original param count to determine arity
        if len(func.params) == 1 and func.params[0].type.name == "unit":
            # Unit parameter - call with Val_unit
            lines.append(
                f'    ml_result = caml_callback(*caml_named_value("{func.name}"), Val_unit);'
            )
        elif len(non_unit_params) == 1:
            # Single non-unit parameter
            lines.append(
                f'    ml_result = caml_callback(*caml_named_value("{func.name}"), ml_{non_unit_params[0].name});'
            )
        elif len(non_unit_params) > 1:
            # Multiple parameters
            param_names = ", ".join(f"ml_{p.name}" for p in non_unit_params)
            lines.append(
                f'    ml_result = caml_callback{len(non_unit_params)}(*caml_named_value("{func.name}"), {param_names});'
            )
        else:
            # No parameters at all (shouldn't happen, but fallback to Val_unit)
            lines.append(
                f'    ml_result = caml_callback(*caml_named_value("{func.name}"), Val_unit);'
            )

        lines.append("")

        # Convert result back to C
        lines.extend(self._convert_ocaml_to_c(func.return_type))

        lines.append("}")

        return lines

    def _convert_c_to_ocaml(self, param_name: str, param_type: IRType) -> list:
        """
        Generate code to convert C value to OCaml value.

        Handles primitives and complex types (options, lists, tuples, custom types).
        """
        lines = []

        if param_type.is_primitive():
            if param_type.name == "string":
                lines.append(f"    ml_{param_name} = caml_copy_string({param_name});")
            elif param_type.name == "int":
                lines.append(f"    ml_{param_name} = Val_int({param_name});")
            elif param_type.name == "float":
                lines.append(f"    ml_{param_name} = caml_copy_double({param_name});")
            elif param_type.name == "bool":
                lines.append(f"    ml_{param_name} = Val_bool({param_name});")
            elif param_type.name == "unit":
                lines.append(f"    ml_{param_name} = Val_unit;")
        elif param_type.kind == TypeKind.LIST:
            # Handle list types - convert C array + length to OCaml list
            lines.extend(self._convert_c_list_to_ocaml(param_name, param_type))
        elif param_type.kind == TypeKind.TUPLE:
            # Handle tuple types - convert C struct to OCaml tuple
            lines.extend(self._convert_c_tuple_to_ocaml(param_name, param_type))
        else:
            # Other complex types (custom types)
            # These are passed as opaque pointers and cast to value
            lines.append(f"    ml_{param_name} = (value){param_name};")

        return lines

    def _convert_ocaml_to_c(self, return_type: IRType) -> list:
        """
        Generate code to convert OCaml return value to C.

        Handles primitives and complex types. Complex types are returned
        as opaque pointers to maintain GC-safety.
        """
        lines = []
        c_type = self._get_c_type(return_type)

        if return_type.is_primitive():
            if return_type.name == "string":
                lines.append(f"    {c_type} result = strdup(String_val(ml_result));")
                lines.append(f"    CAMLreturnT({c_type}, result);")
            elif return_type.name == "int":
                lines.append(f"    {c_type} result = Int_val(ml_result);")
                lines.append(f"    CAMLreturnT({c_type}, result);")
            elif return_type.name == "float":
                lines.append(f"    {c_type} result = Double_val(ml_result);")
                lines.append(f"    CAMLreturnT({c_type}, result);")
            elif return_type.name == "bool":
                lines.append(f"    {c_type} result = Bool_val(ml_result);")
                lines.append(f"    CAMLreturnT({c_type}, result);")
            elif return_type.name == "unit":
                lines.append("    CAMLreturn0;")
        elif return_type.kind == TypeKind.OPTION:
            # Handle option types: None -> NULL, Some(x) -> unwrap and convert x
            lines.append("    /* Handle option type: None = NULL, Some(x) = unwrap x */")
            lines.append("    if (ml_result == Val_int(0)) {")
            lines.append("        /* None case */")
            lines.append(f"        CAMLreturnT({c_type}, NULL);")
            lines.append("    } else {")
            lines.append("        /* Some case - extract the value */")
            lines.append("        value ml_some_value = Field(ml_result, 0);")

            # Convert the inner value based on its type
            if return_type.params and return_type.params[0].is_primitive():
                inner_type = return_type.params[0]
                if inner_type.name == "string":
                    lines.append(f"        {c_type} result = strdup(String_val(ml_some_value));")
                elif inner_type.name == "int":
                    # For int option, we need to return a pointer to int
                    # Allocate memory for the int value
                    lines.append(f"        int* result = (int*)malloc(sizeof(int));")
                    lines.append(f"        *result = Int_val(ml_some_value);")
                elif inner_type.name == "float":
                    lines.append(f"        double* result = (double*)malloc(sizeof(double));")
                    lines.append(f"        *result = Double_val(ml_some_value);")
                elif inner_type.name == "bool":
                    lines.append(f"        int* result = (int*)malloc(sizeof(int));")
                    lines.append(f"        *result = Bool_val(ml_some_value);")
                else:
                    # Default to opaque pointer
                    lines.append(f"        {c_type} result = (void*)ml_some_value;")
            else:
                # For complex inner types, return as opaque pointer
                lines.append(f"        {c_type} result = (void*)ml_some_value;")

            lines.append(f"        CAMLreturnT({c_type}, result);")
            lines.append("    }")
        elif return_type.kind == TypeKind.LIST:
            # Handle list return types - convert OCaml list to C array
            lines.extend(self._convert_ocaml_list_to_c(return_type))
        elif return_type.kind == TypeKind.TUPLE:
            # Handle tuple return types - convert OCaml tuple to C struct
            lines.extend(self._convert_ocaml_tuple_to_c(return_type))
        else:
            # Other complex types (custom types)
            # Return as opaque pointer (cast from value)
            # Note: This keeps the value alive and GC-safe
            lines.append(f"    {c_type} result = (void*)ml_result;")
            lines.append(f"    CAMLreturnT({c_type}, result);")

        return lines

    def _convert_c_list_to_ocaml(self, param_name: str, param_type: IRType) -> list:
        """
        Generate code to convert C list (array + length) to OCaml list.

        For list parameters, we expect the Python side to pass:
        - A pointer to the array data
        - The length of the array (passed as a separate parameter)
        """
        lines = []

        if not param_type.params:
            # No element type info, fall back to opaque pointer
            lines.append(f"    ml_{param_name} = (value){param_name};")
            return lines

        element_type = param_type.params[0]

        # Special case: nested list (e.g., int list list)
        if (
            element_type.kind == TypeKind.LIST
            and element_type.params
            and element_type.params[0].is_primitive()
        ):
            return self._convert_c_nested_list_to_ocaml(param_name, element_type)

        if not element_type.is_primitive():
            # For other complex element types, fall back to opaque pointer
            lines.append(f"    ml_{param_name} = (value){param_name};")
            return lines

        # Cast void* to appropriate array type
        lines.append(f"    /* Convert C array to OCaml list */")
        if element_type.name == "string":
            lines.append(f"    const char** {param_name}_arr = (const char**){param_name};")
        elif element_type.name == "int":
            lines.append(f"    int* {param_name}_arr = (int*){param_name};")
        elif element_type.name == "float":
            lines.append(f"    double* {param_name}_arr = (double*){param_name};")
        elif element_type.name == "bool":
            lines.append(f"    int* {param_name}_arr = (int*){param_name};")

        # Build OCaml list from C array in reverse order (for efficiency)
        lines.append(f"    CAMLlocal1(cons);")
        lines.append(f"    ml_{param_name} = Val_emptylist;  /* Start with empty list */")
        lines.append(f"    for (int i = {param_name}_len - 1; i >= 0; i--) {{")

        # Create cons cell
        lines.append(f"        cons = caml_alloc(2, 0);  /* Allocate cons cell */")

        # Convert and store the element
        if element_type.name == "string":
            lines.append(f"        Store_field(cons, 0, caml_copy_string({param_name}_arr[i]));")
        elif element_type.name == "int":
            lines.append(f"        Store_field(cons, 0, Val_int({param_name}_arr[i]));")
        elif element_type.name == "float":
            lines.append(f"        Store_field(cons, 0, caml_copy_double({param_name}_arr[i]));")
        elif element_type.name == "bool":
            lines.append(f"        Store_field(cons, 0, Val_bool({param_name}_arr[i]));")

        # Link to rest of list
        lines.append(f"        Store_field(cons, 1, ml_{param_name});")
        lines.append(f"        ml_{param_name} = cons;")
        lines.append(f"    }}")

        return lines

    def _convert_c_nested_list_to_ocaml(self, param_name: str, inner_list_type: IRType) -> list:
        """
        Generate code to convert C nested list (list of lists) to OCaml list list.

        For nested lists like int list list, we expect:
        - A void*** array where each element is a [length, array_ptr] pair
        - The outer length (passed as a separate parameter)
        """
        lines = []

        if not inner_list_type.params or not inner_list_type.params[0].is_primitive():
            # Only support primitive inner types for now
            lines.append(f"    ml_{param_name} = (value){param_name};")
            return lines

        inner_element_type = inner_list_type.params[0]

        lines.append(f"    /* Convert C nested list to OCaml list list */")
        lines.append(f"    void*** {param_name}_arr = (void***){param_name};")
        lines.append(f"    CAMLlocal1(inner_list);")
        lines.append(f"    CAMLlocal1(inner_cons);")
        lines.append(f"    CAMLlocal1(outer_cons);")
        lines.append(f"    ml_{param_name} = Val_emptylist;  /* Start with empty list */")
        lines.append(f"    for (int i = {param_name}_len - 1; i >= 0; i--) {{")
        lines.append(f"        /* Each element is [length, array_ptr] */")
        lines.append(f"        void** inner_pair = {param_name}_arr[i];")
        lines.append(f"        int inner_len = (int)(intptr_t)inner_pair[0];")

        # Convert inner array based on type
        if inner_element_type.name == "int":
            lines.append(f"        int* inner_arr = (int*)inner_pair[1];")
        elif inner_element_type.name == "float":
            lines.append(f"        double* inner_arr = (double*)inner_pair[1];")
        elif inner_element_type.name == "string":
            lines.append(f"        const char** inner_arr = (const char**)inner_pair[1];")
        elif inner_element_type.name == "bool":
            lines.append(f"        int* inner_arr = (int*)inner_pair[1];")

        # Build inner OCaml list
        lines.append(f"        inner_list = Val_emptylist;")
        lines.append(f"        for (int j = inner_len - 1; j >= 0; j--) {{")
        lines.append(f"            inner_cons = caml_alloc(2, 0);")

        # Store element based on type
        if inner_element_type.name == "string":
            lines.append(f"            Store_field(inner_cons, 0, caml_copy_string(inner_arr[j]));")
        elif inner_element_type.name == "int":
            lines.append(f"            Store_field(inner_cons, 0, Val_int(inner_arr[j]));")
        elif inner_element_type.name == "float":
            lines.append(f"            Store_field(inner_cons, 0, caml_copy_double(inner_arr[j]));")
        elif inner_element_type.name == "bool":
            lines.append(f"            Store_field(inner_cons, 0, Val_bool(inner_arr[j]));")

        lines.append(f"            Store_field(inner_cons, 1, inner_list);")
        lines.append(f"            inner_list = inner_cons;")
        lines.append(f"        }}")

        # Add inner list to outer list
        lines.append(f"        outer_cons = caml_alloc(2, 0);")
        lines.append(f"        Store_field(outer_cons, 0, inner_list);")
        lines.append(f"        Store_field(outer_cons, 1, ml_{param_name});")
        lines.append(f"        ml_{param_name} = outer_cons;")
        lines.append(f"    }}")

        return lines

    def _convert_ocaml_list_to_c(self, return_type: IRType) -> list:
        """
        Generate code to convert OCaml list to C array.

        Returns a struct containing the array pointer and length.
        """
        lines = []

        if not return_type.params:
            # No element type specified, fall back to opaque pointer
            lines.append(f"    void* result = (void*)ml_result;")
            lines.append(f"    CAMLreturnT(void*, result);")
            return lines

        element_type = return_type.params[0]

        # Handle lists of tuples specially
        if element_type.kind == TypeKind.TUPLE:
            return self._convert_ocaml_list_of_tuples_to_c(element_type)

        # For other complex element types, fall back to opaque pointer
        if not element_type.is_primitive():
            lines.append(f"    void* result = (void*)ml_result;")
            lines.append(f"    CAMLreturnT(void*, result);")
            return lines

        # First pass: count list length
        lines.append(f"    /* Convert OCaml list to C array */")
        lines.append(f"    int list_len = 0;")
        lines.append(f"    value temp_list = ml_result;")
        lines.append(f"    while (temp_list != Val_emptylist) {{")
        lines.append(f"        list_len++;")
        lines.append(f"        temp_list = Field(temp_list, 1);  /* tail */")
        lines.append(f"    }}")
        lines.append(f"")

        # Allocate result struct (length + array pointer)
        lines.append(f"    /* Allocate result: [length, array_ptr] */")
        lines.append(f"    void** result = (void**)malloc(2 * sizeof(void*));")
        lines.append(f"    result[0] = (void*)(intptr_t)list_len;")
        lines.append(f"")

        # Handle empty list
        lines.append(f"    if (list_len == 0) {{")
        lines.append(f"        result[1] = NULL;")
        lines.append(f"        CAMLreturnT(void*, result);")
        lines.append(f"    }}")
        lines.append(f"")

        # Allocate array for elements
        if element_type.name == "string":
            lines.append(
                f"    const char** array = (const char**)malloc(list_len * sizeof(const char*));"
            )
        elif element_type.name == "int":
            lines.append(f"    int* array = (int*)malloc(list_len * sizeof(int));")
        elif element_type.name == "float":
            lines.append(f"    double* array = (double*)malloc(list_len * sizeof(double));")
        elif element_type.name == "bool":
            lines.append(f"    int* array = (int*)malloc(list_len * sizeof(int));")

        # Second pass: extract elements
        lines.append(f"")
        lines.append(f"    temp_list = ml_result;")
        lines.append(f"    for (int i = 0; i < list_len; i++) {{")
        lines.append(f"        value head = Field(temp_list, 0);  /* head */")

        if element_type.name == "string":
            lines.append(f"        array[i] = strdup(String_val(head));")
        elif element_type.name == "int":
            lines.append(f"        array[i] = Int_val(head);")
        elif element_type.name == "float":
            lines.append(f"        array[i] = Double_val(head);")
        elif element_type.name == "bool":
            lines.append(f"        array[i] = Bool_val(head);")

        lines.append(f"        temp_list = Field(temp_list, 1);  /* tail */")
        lines.append(f"    }}")
        lines.append(f"")
        lines.append(f"    result[1] = array;")
        lines.append(f"    CAMLreturnT(void*, result);")

        return lines

    def _convert_ocaml_list_of_tuples_to_c(self, tuple_type: IRType) -> list:
        """
        Generate code to convert OCaml list of tuples to C array of tuple arrays.

        Each tuple is represented as void** array, and the list is an array of these.
        Returns a struct containing [length, array_of_tuples].
        """
        lines = []

        # Only handle tuples with all primitive elements
        if not tuple_type.params or not all(p.is_primitive() for p in tuple_type.params):
            # Fall back to opaque pointer for complex tuples
            lines.append(f"    void* result = (void*)ml_result;")
            lines.append(f"    CAMLreturnT(void*, result);")
            return lines

        tuple_size = len(tuple_type.params)

        # First pass: count list length
        lines.append(f"    /* Convert OCaml list of tuples to C array */")
        lines.append(f"    int list_len = 0;")
        lines.append(f"    value temp_list = ml_result;")
        lines.append(f"    while (temp_list != Val_emptylist) {{")
        lines.append(f"        list_len++;")
        lines.append(f"        temp_list = Field(temp_list, 1);  /* tail */")
        lines.append(f"    }}")
        lines.append(f"")

        # Allocate result struct (length + array pointer)
        lines.append(f"    /* Allocate result: [length, array_of_tuples] */")
        lines.append(f"    void** result = (void**)malloc(2 * sizeof(void*));")
        lines.append(f"    result[0] = (void*)(intptr_t)list_len;")
        lines.append(f"")

        # Handle empty list
        lines.append(f"    if (list_len == 0) {{")
        lines.append(f"        result[1] = NULL;")
        lines.append(f"        CAMLreturnT(void*, result);")
        lines.append(f"    }}")
        lines.append(f"")

        # Allocate array of tuple pointers
        lines.append(f"    void*** tuple_array = (void***)malloc(list_len * sizeof(void**));")
        lines.append(f"")

        # Second pass: extract tuples
        lines.append(f"    temp_list = ml_result;")
        lines.append(f"    for (int i = 0; i < list_len; i++) {{")
        lines.append(f"        value tuple = Field(temp_list, 0);  /* head */")
        lines.append(f"")
        lines.append(f"        /* Allocate array for tuple elements */")
        lines.append(f"        void** tuple_elem = (void**)malloc({tuple_size} * sizeof(void*));")
        lines.append(f"")

        # Extract each tuple element
        for j, elem_type in enumerate(tuple_type.params):
            lines.append(f"        value elem_{j} = Field(tuple, {j});")
            if elem_type.name == "string":
                lines.append(f"        tuple_elem[{j}] = (void*)strdup(String_val(elem_{j}));")
            elif elem_type.name == "int":
                lines.append(f"        tuple_elem[{j}] = (void*)(intptr_t)Int_val(elem_{j});")
            elif elem_type.name == "float":
                lines.append(f"        double* float_val_{j} = (double*)malloc(sizeof(double));")
                lines.append(f"        *float_val_{j} = Double_val(elem_{j});")
                lines.append(f"        tuple_elem[{j}] = (void*)float_val_{j};")
            elif elem_type.name == "bool":
                lines.append(f"        tuple_elem[{j}] = (void*)(intptr_t)Bool_val(elem_{j});")

        lines.append(f"")
        lines.append(f"        tuple_array[i] = tuple_elem;")
        lines.append(f"        temp_list = Field(temp_list, 1);  /* tail */")
        lines.append(f"    }}")
        lines.append(f"")
        lines.append(f"    result[1] = tuple_array;")
        lines.append(f"    CAMLreturnT(void*, result);")

        return lines

    def _convert_c_tuple_to_ocaml(self, param_name: str, param_type: IRType) -> list:
        """
        Generate code to convert C tuple (void* to struct) to OCaml tuple.

        For tuple parameters, we expect a void* pointer to a struct containing the tuple elements.
        """
        lines = []

        if not param_type.params or not all(p.is_primitive() for p in param_type.params):
            # For complex element types, fall back to opaque pointer
            lines.append(f"    ml_{param_name} = (value){param_name};")
            return lines

        # Cast void* to appropriate struct pointer and extract elements
        lines.append(f"    /* Convert C tuple to OCaml tuple */")
        lines.append(f"    void** {param_name}_arr = (void**){param_name};")

        # Allocate OCaml tuple
        tuple_size = len(param_type.params)
        lines.append(f"    ml_{param_name} = caml_alloc({tuple_size}, 0);")

        # Store each element in the tuple
        for i, elem_type in enumerate(param_type.params):
            if elem_type.name == "string":
                lines.append(
                    f"    Store_field(ml_{param_name}, {i}, caml_copy_string((const char*){param_name}_arr[{i}]));"
                )
            elif elem_type.name == "int":
                lines.append(
                    f"    Store_field(ml_{param_name}, {i}, Val_int((intptr_t){param_name}_arr[{i}]));"
                )
            elif elem_type.name == "float":
                lines.append(
                    f"    Store_field(ml_{param_name}, {i}, caml_copy_double(*(double*){param_name}_arr[{i}]));"
                )
            elif elem_type.name == "bool":
                lines.append(
                    f"    Store_field(ml_{param_name}, {i}, Val_bool((intptr_t){param_name}_arr[{i}]));"
                )

        return lines

    def _convert_ocaml_tuple_to_c(self, return_type: IRType) -> list:
        """
        Generate code to convert OCaml tuple to C struct (void* array).

        Returns a void** array containing pointers to the tuple elements.
        """
        lines = []

        if not return_type.params or not all(p.is_primitive() for p in return_type.params):
            # For complex element types, fall back to opaque pointer
            lines.append(f"    void* result = (void*)ml_result;")
            lines.append(f"    CAMLreturnT(void*, result);")
            return lines

        tuple_size = len(return_type.params)

        # Allocate result array
        lines.append(f"    /* Convert OCaml tuple to C array */")
        lines.append(f"    void** result = (void**)malloc({tuple_size} * sizeof(void*));")

        # Extract each element from the OCaml tuple
        for i, elem_type in enumerate(return_type.params):
            lines.append(f"    value elem_{i} = Field(ml_result, {i});")

            if elem_type.name == "string":
                lines.append(f"    result[{i}] = (void*)strdup(String_val(elem_{i}));")
            elif elem_type.name == "int":
                lines.append(f"    result[{i}] = (void*)(intptr_t)Int_val(elem_{i});")
            elif elem_type.name == "float":
                lines.append(f"    double* float_val_{i} = (double*)malloc(sizeof(double));")
                lines.append(f"    *float_val_{i} = Double_val(elem_{i});")
                lines.append(f"    result[{i}] = (void*)float_val_{i};")
            elif elem_type.name == "bool":
                lines.append(f"    result[{i}] = (void*)(intptr_t)Bool_val(elem_{i});")

        lines.append(f"    CAMLreturnT(void*, result);")

        return lines

    def _get_c_type(self, ir_type: IRType) -> str:
        """
        Convert IR type to C type string.

        Primitives map to C native types.
        Options of primitives map to pointer types (nullable).
        Other complex types map to void* (opaque).
        """
        if ir_type.is_primitive():
            return self.C_TYPE_MAP.get(ir_type.name, "char*")

        # Handle option types specially - they become nullable pointers
        if ir_type.kind == TypeKind.OPTION:
            if ir_type.params and ir_type.params[0].is_primitive():
                inner_type = ir_type.params[0]
                if inner_type.name == "string":
                    return "const char*"  # Nullable string
                elif inner_type.name == "int":
                    return "int*"  # Pointer to int (NULL = None)
                elif inner_type.name == "float":
                    return "double*"  # Pointer to double
                elif inner_type.name == "bool":
                    return "int*"  # Pointer to bool
            # For option of complex types, use void*
            return "void*"

        # Other complex types are opaque pointers in C
        if ir_type.kind in (
            TypeKind.LIST,
            TypeKind.TUPLE,
            TypeKind.CUSTOM,
            TypeKind.RECORD,
            TypeKind.VARIANT,
        ):
            return "void*"

        raise ValueError(f"Unsupported type for C generation: {ir_type}")

Functions

generate_stubs(module, module_name)

Generate C stub implementation file.

Source code in src/polyglot_ffi/generators/c_stubs_gen.py
def generate_stubs(self, module: IRModule, module_name: str) -> str:
    """Generate C stub implementation file."""
    safe_name = sanitize_module_name(module_name)
    lines = [
        f"/* Generated by polyglot-ffi */",
        f"/* {safe_name}_stubs.c */",
        "",
        "#include <string.h>",
        "#include <stdlib.h>",
        "#include <stdint.h>",
        "#include <caml/mlvalues.h>",
        "#include <caml/memory.h>",
        "#include <caml/alloc.h>",
        "#include <caml/callback.h>",
        "",
        "/* OCaml runtime initialization - call once before using any functions */",
        "static int _ocaml_initialized = 0;",
        "",
        "void ml_init(void) {",
        "    if (!_ocaml_initialized) {",
        "        char* argv[] = {NULL};",
        "        caml_startup(argv);",
        "        _ocaml_initialized = 1;",
        "    }",
        "}",
        "",
        "/* Memory cleanup functions */",
        "",
        "/* Free option type results (int*, double*, etc.) */",
        "void ml_free_option(void* ptr) {",
        "    if (ptr) {",
        "        free(ptr);",
        "    }",
        "}",
        "",
        "/* Free list results returned by ml_* functions */",
        "/* For primitive lists (int, float, bool), just frees the structure */",
        "void ml_free_list_result(void* result) {",
        "    if (result) {",
        "        void** res_array = (void**)result;",
        "        if (res_array[1]) {",
        "            free(res_array[1]);  // Free array",
        "        }",
        "        free(result);  // Free result struct",
        "    }",
        "}",
        "",
        "/* For string lists, frees each string and the structure */",
        "void ml_free_string_list_result(void* result) {",
        "    if (result) {",
        "        void** res_array = (void**)result;",
        "        int len = (int)(intptr_t)res_array[0];",
        "        if (res_array[1]) {",
        "            const char** str_array = (const char**)res_array[1];",
        "            for (int i = 0; i < len; i++) {",
        "                if (str_array[i]) {",
        "                    free((void*)str_array[i]);  // Free each string",
        "                }",
        "            }",
        "            free(str_array);  // Free array",
        "        }",
        "        free(result);  // Free result struct",
        "    }",
        "}",
        "",
        "/* For tuple lists, frees each tuple and the structure */",
        "void ml_free_tuple_list_result(void* result) {",
        "    if (result) {",
        "        void** res_array = (void**)result;",
        "        int len = (int)(intptr_t)res_array[0];",
        "        if (res_array[1]) {",
        "            void** tuple_array = (void**)res_array[1];",
        "            for (int i = 0; i < len; i++) {",
        "                if (tuple_array[i]) {",
        "                    free(tuple_array[i]);  // Free each tuple",
        "                }",
        "            }",
        "            free(tuple_array);  // Free array",
        "        }",
        "        free(result);  // Free result struct",
        "    }",
        "}",
        "",
    ]

    for func in module.functions:
        lines.extend(self._generate_function_stub(func))
        lines.append("")

    return "\n".join(lines)
generate_header(module, module_name)

Generate C header file.

Source code in src/polyglot_ffi/generators/c_stubs_gen.py
def generate_header(self, module: IRModule, module_name: str) -> str:
    """Generate C header file."""
    safe_name = sanitize_module_name(module_name)
    guard = f"{safe_name.upper()}_STUBS_H"

    lines = [
        f"/* Generated by polyglot-ffi */",
        f"/* {safe_name}_stubs.h */",
        "",
        f"#ifndef {guard}",
        f"#define {guard}",
        "",
        "/* Initialize OCaml runtime - must be called before any other functions */",
        "void ml_init(void);",
        "",
    ]

    # Function declarations
    for func in module.functions:
        c_return = self._get_c_type(func.return_type)
        # Filter out unit parameters - they should not appear in C signatures
        non_unit_params = [p for p in func.params if p.type.name != "unit"]
        if non_unit_params:
            param_parts = []
            for p in non_unit_params:
                param_parts.append(f"{self._get_c_type(p.type)} {p.name}")
                # For list types, add a length parameter
                if p.type.kind == TypeKind.LIST:
                    param_parts.append(f"int {p.name}_len")
            params = ", ".join(param_parts)
        else:
            params = "void"
        lines.append(f"{c_return} ml_{func.name}({params});")

    lines.append("")
    lines.append("/* Memory cleanup functions */")
    lines.append("/* NOTE: Caller must free returned pointers for option and list types */")
    lines.append("")
    lines.append("/* Free option type results (int*, double*, etc.) */")
    lines.append("void ml_free_option(void* ptr);")
    lines.append("")
    lines.append("/* Free list results returned by ml_* functions */")
    lines.append("/* For primitive lists (int, float, bool), just frees the structure */")
    lines.append("void ml_free_list_result(void* result);")
    lines.append("")
    lines.append("/* For string lists, frees each string and the structure */")
    lines.append("void ml_free_string_list_result(void* result);")
    lines.append("")
    lines.append("/* For tuple lists, frees each tuple and the structure */")
    lines.append("void ml_free_tuple_list_result(void* result);")
    lines.append("")
    lines.append(f"#endif /* {guard} */")

    return "\n".join(lines)

Usage Example

from polyglot_ffi.generators.c_stubs_gen import CStubGenerator

generator = CStubGenerator()
stubs = generator.generate_stubs(module, "crypto")
header = generator.generate_header(module, "crypto")

Path("stubs.c").write_text(stubs)
Path("stubs.h").write_text(header)

Generated Output

For val encrypt : string -> string:

/* stubs.c */
#include <caml/mlvalues.h>
#include <caml/memory.h>
#include <caml/alloc.h>
#include "stubs.h"

CAMLprim value ml_encrypt(value input) {
    CAMLparam1(input);
    CAMLlocal1(result);

    char* c_input = String_val(input);
    char* c_result = encrypt(c_input);
    result = caml_copy_string(c_result);

    CAMLreturn(result);
}
/* stubs.h */
#ifndef CRYPTO_STUBS_H
#define CRYPTO_STUBS_H

#include <caml/mlvalues.h>

CAMLprim value ml_encrypt(value input);

#endif

Python Generator

Generates type-safe Python wrapper modules with type hints.

polyglot_ffi.generators.python_gen.PythonGenerator

Generate Python wrapper code.

Source code in src/polyglot_ffi/generators/python_gen.py
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
class PythonGenerator:
    """Generate Python wrapper code."""

    # Map IR types to Python type hints
    PY_TYPE_MAP: Dict[str, str] = {
        "string": "str",
        "int": "int",
        "float": "float",
        "bool": "bool",
        "unit": "None",
    }

    # Map IR types to ctypes
    CTYPES_MAP: Dict[str, str] = {
        "string": "ctypes.c_char_p",
        "int": "ctypes.c_int",
        "float": "ctypes.c_double",
        "bool": "ctypes.c_bool",
        "unit": "None",
    }

    @staticmethod
    def _sanitize_identifier(name: str) -> str:
        """Sanitize a name to be a valid Python identifier."""
        # Replace hyphens and other non-identifier chars with underscores
        sanitized = name.replace("-", "_").replace(".", "_")
        # Ensure it starts with a letter or underscore
        if sanitized and not (sanitized[0].isalpha() or sanitized[0] == "_"):
            sanitized = "_" + sanitized
        return sanitized

    def generate(self, module: IRModule, module_name: str) -> str:
        """Generate Python wrapper module."""
        # Sanitize module name for Python identifiers and filenames
        safe_name = sanitize_module_name(module_name)
        error_class = safe_name.capitalize() + "Error"

        lines = [
            "# Generated by polyglot-ffi",
            f"# {safe_name}_py.py",
            "",
            "import ctypes",
            "import sys",
            "from pathlib import Path",
            "from typing import Optional, List, Tuple, Any",
            "",
            "# Determine library extension based on platform",
            "if sys.platform == 'darwin':",
            "    _lib_ext = 'dylib'",
            "elif sys.platform == 'win32':",
            "    _lib_ext = 'dll'",
            "else:",
            "    _lib_ext = 'so'",
            "",
            "# Load the shared library",
            f'_lib_path = Path(__file__).parent / f"lib{safe_name}.{{_lib_ext}}"',
            "_lib = ctypes.CDLL(str(_lib_path))",
            "",
            "# Initialize OCaml runtime",
            "_lib.ml_init.argtypes = []",
            "_lib.ml_init.restype = None",
            "_lib.ml_init()",
            "",
            "# Configure memory cleanup functions",
            "_lib.ml_free_option.argtypes = [ctypes.c_void_p]",
            "_lib.ml_free_option.restype = None",
            "_lib.ml_free_list_result.argtypes = [ctypes.c_void_p]",
            "_lib.ml_free_list_result.restype = None",
            "_lib.ml_free_string_list_result.argtypes = [ctypes.c_void_p]",
            "_lib.ml_free_string_list_result.restype = None",
            "_lib.ml_free_tuple_list_result.argtypes = [ctypes.c_void_p]",
            "_lib.ml_free_tuple_list_result.restype = None",
            "",
            f"class {error_class}(Exception):",
            f'    """Raised when {module_name} operations fail"""',
            "    pass",
            "",
        ]

        # Configure ctypes for each function
        for func in module.functions:
            lines.append(f"# Configure {func.name}")

            # Set argtypes (filter out unit parameters - they don't appear in C signatures)
            non_unit_params = [p for p in func.params if p.type.name != "unit"]
            if non_unit_params:
                argtypes = []
                for p in non_unit_params:
                    argtypes.append(self._get_ctypes(p.type))
                    # For list types, add a length parameter (ctypes.c_int)
                    if p.type.kind == TypeKind.LIST:
                        argtypes.append("ctypes.c_int")
                lines.append(f"_lib.ml_{func.name}.argtypes = [{', '.join(argtypes)}]")
            else:
                lines.append(f"_lib.ml_{func.name}.argtypes = []")

            # Set restype
            restype = self._get_ctypes(func.return_type)
            lines.append(f"_lib.ml_{func.name}.restype = {restype}")
            lines.append("")

        # Generate wrapper functions
        for func in module.functions:
            lines.extend(self._generate_function_wrapper(func, module_name, error_class))
            lines.append("")

        return "\n".join(lines)

    def _generate_function_wrapper(
        self, func: IRFunction, module_name: str, error_class: str
    ) -> list:
        """Generate Python wrapper for a single function."""
        lines = []

        # Function signature (filter out unit parameters from Python signature)
        non_unit_params = [p for p in func.params if p.type.name != "unit"]
        params = [f"{p.name}: {self._get_py_type(p.type)}" for p in non_unit_params]
        params_str = ", ".join(params) if params else ""
        return_type = self._get_py_type(func.return_type)

        lines.append(f"def {func.name}({params_str}) -> {return_type}:")

        # Docstring
        if func.doc:
            lines.append(f'    """{func.doc}"""')
        else:
            lines.append(f'    """Call OCaml {func.name} function"""')

        lines.append("    try:")

        # Convert arguments (only non-unit parameters)
        call_args = []
        for param in non_unit_params:
            if param.type.kind == TypeKind.LIST:
                # Convert Python list to C array + length
                lines.extend(self._generate_list_to_c_conversion(param))
                call_args.append(f"{param.name}_array")
                call_args.append(f"{param.name}_len")
            elif param.type.kind == TypeKind.TUPLE:
                # Convert Python tuple to C array
                lines.extend(self._generate_tuple_to_c_conversion(param))
                call_args.append(f"{param.name}_array")
            elif param.type.name == "string":
                call_args.append(f"{param.name}.encode('utf-8')")
            else:
                call_args.append(param.name)

        # Make the call
        call_str = ", ".join(call_args) if call_args else ""
        lines.append(f"        result = _lib.ml_{func.name}({call_str})")

        # Handle return value
        if func.return_type.kind == TypeKind.LIST:
            # Handle list return types - convert C array to Python list
            lines.extend(self._generate_c_to_list_conversion(func.return_type))
        elif func.return_type.kind == TypeKind.TUPLE:
            # Handle tuple return types - convert C array to Python tuple
            lines.extend(self._generate_c_to_tuple_conversion(func.return_type))
        elif func.return_type.kind == TypeKind.OPTION:
            # Handle option types: None = NULL pointer, Some(x) = unwrap value
            lines.append("        # Handle option type: NULL = None, otherwise unwrap value")
            if func.return_type.params and func.return_type.params[0].is_primitive():
                inner_type = func.return_type.params[0]
                if inner_type.name == "string":
                    lines.append("        if not result:")
                    lines.append("            return None")
                    lines.append("        # Result is c_void_p, convert to string and free")
                    lines.append("        # Copy string from C memory to Python before freeing")
                    lines.append("        value = ctypes.string_at(result).decode('utf-8')")
                    lines.append("        # Clean up C-allocated string (strdup)")
                    lines.append("        _lib.ml_free_option(result)")
                    lines.append("        return value")
                elif inner_type.name in ("int", "bool", "float"):
                    lines.append("        if result is None:")
                    lines.append("            return None")
                    lines.append("        try:")
                    lines.append("            value = result[0]  # Dereference pointer")
                    lines.append("        except (ValueError, TypeError):")
                    lines.append("            return None")
                    lines.append("        # Clean up C-allocated memory")
                    lines.append("        _lib.ml_free_option(result)")
                    lines.append("        return value")
                else:
                    lines.append("        return result if result else None")
            else:
                lines.append("        return result if result else None")
        elif func.return_type.name == "string":
            lines.append("        if result is None:")
            lines.append(f'            raise {error_class}("{func.name} returned NULL")')
            lines.append("        return result.decode('utf-8')")
        elif func.return_type.name == "unit":
            lines.append("        return None")
        else:
            lines.append("        return result")

        # Error handling
        lines.append("    except Exception as e:")
        lines.append(f'        raise {error_class}(f"{func.name} failed: {{e}}")')

        return lines

    def _get_py_type(self, ir_type: IRType) -> str:
        """
        Convert IR type to Python type hint.

        Handles:
        - Primitives: str, int, float, bool, None
        - Options: Optional[T]
        - Lists: List[T]
        - Tuples: Tuple[T1, T2, ...]
        - Custom types: Class names
        """
        if ir_type.is_primitive():
            return self.PY_TYPE_MAP.get(ir_type.name, "str")

        # Handle option types
        if ir_type.kind == TypeKind.OPTION:
            if ir_type.params:
                inner_type = self._get_py_type(ir_type.params[0])
                return f"Optional[{inner_type}]"
            return "Optional[Any]"

        # Handle list types
        if ir_type.kind == TypeKind.LIST:
            if ir_type.params:
                inner_type = self._get_py_type(ir_type.params[0])
                return f"List[{inner_type}]"
            return "List[Any]"

        # Handle tuple types
        if ir_type.kind == TypeKind.TUPLE:
            if ir_type.params:
                tuple_types = [self._get_py_type(p) for p in ir_type.params]
                return f"Tuple[{', '.join(tuple_types)}]"
            return "Tuple[Any, ...]"

        # Handle custom types (records, variants)
        if ir_type.kind in (TypeKind.CUSTOM, TypeKind.RECORD, TypeKind.VARIANT):
            # Use the type name as a class name (with capitalization)
            return ir_type.name.capitalize()

        raise ValueError(f"Unsupported type for Python generation: {ir_type}")

    def _get_ctypes(self, ir_type: IRType) -> str:
        """
        Convert IR type to ctypes type.

        For option types of primitives, use the appropriate pointer type.
        For other complex types, use c_void_p as they're opaque pointers.
        """
        if ir_type.is_primitive():
            return self.CTYPES_MAP.get(ir_type.name, "ctypes.c_char_p")

        # Handle option types - they become nullable pointers
        if ir_type.kind == TypeKind.OPTION:
            if ir_type.params and ir_type.params[0].is_primitive():
                inner_type = ir_type.params[0]
                if inner_type.name == "string":
                    # Use c_void_p for strings to avoid automatic ctypes memory management
                    # We'll manually convert and free the string
                    return "ctypes.c_void_p"  # Nullable string pointer
                elif inner_type.name == "int":
                    return "ctypes.POINTER(ctypes.c_int)"  # Pointer to int
                elif inner_type.name == "float":
                    return "ctypes.POINTER(ctypes.c_double)"  # Pointer to double
                elif inner_type.name == "bool":
                    return "ctypes.POINTER(ctypes.c_bool)"  # Pointer to bool
            # For option of complex types, use void*
            return "ctypes.c_void_p"

        # Other complex types are passed as opaque pointers
        if ir_type.kind in (
            TypeKind.LIST,
            TypeKind.TUPLE,
            TypeKind.CUSTOM,
            TypeKind.RECORD,
            TypeKind.VARIANT,
        ):
            return "ctypes.c_void_p"

        raise ValueError(f"Unsupported type for ctypes: {ir_type}")

    def _generate_list_to_c_conversion(self, param: "IRParameter") -> list:
        """Generate code to convert Python list to C array + length."""
        lines = []
        param_name = param.name

        if not param.type.params:
            # No element type info
            return lines

        element_type = param.type.params[0]

        # Special case: nested list (e.g., List[List[int]])
        if (
            element_type.kind == TypeKind.LIST
            and element_type.params
            and element_type.params[0].is_primitive()
        ):
            return self._generate_nested_list_to_c_conversion(param, element_type)

        if not element_type.is_primitive():
            # For other complex element types, not implemented yet
            return lines

        lines.append("        # Convert Python list to C array")
        lines.append(f"        {param_name}_len = len({param_name})")

        # Convert list to ctypes array
        if element_type.name == "string":
            lines.append(
                f"        {param_name}_encoded = [s.encode('utf-8') for s in {param_name}]"
            )
            lines.append(
                f"        {param_name}_array = (ctypes.c_char_p * {param_name}_len)"
                f"(*{param_name}_encoded)"
            )
        elif element_type.name == "int":
            lines.append(
                f"        {param_name}_array = (ctypes.c_int * {param_name}_len)(*{param_name})"
            )
        elif element_type.name == "float":
            lines.append(
                f"        {param_name}_array = (ctypes.c_double * {param_name}_len)(*{param_name})"
            )
        elif element_type.name == "bool":
            lines.append(
                f"        {param_name}_array = (ctypes.c_bool * {param_name}_len)(*{param_name})"
            )

        return lines

    def _generate_nested_list_to_c_conversion(
        self, param: "IRParameter", inner_list_type: IRType
    ) -> list:
        """Generate code to convert Python nested list (List[List[T]]) to C array of arrays."""
        lines = []
        param_name = param.name

        if not inner_list_type.params or not inner_list_type.params[0].is_primitive():
            # Only support primitive inner types
            return lines

        inner_element_type = inner_list_type.params[0]

        lines.append("        # Convert Python nested list to C array of arrays")
        lines.append(f"        {param_name}_len = len({param_name})")
        lines.append(f"        {param_name}_array = (ctypes.c_void_p * {param_name}_len)()")
        lines.append(f"        for i, inner_list in enumerate({param_name}):")
        lines.append("            inner_len = len(inner_list)")
        lines.append("            # Create [length, array_ptr] pair for each inner list")
        lines.append("            inner_pair = (ctypes.c_void_p * 2)()")
        lines.append("            inner_pair[0] = ctypes.c_void_p(inner_len)")

        # Convert inner list based on type
        if inner_element_type.name == "string":
            lines.append("            inner_encoded = [s.encode('utf-8') for s in inner_list]")
            lines.append("            inner_arr = (ctypes.c_char_p * inner_len)(*inner_encoded)")
        elif inner_element_type.name == "int":
            lines.append("            inner_arr = (ctypes.c_int * inner_len)(*inner_list)")
        elif inner_element_type.name == "float":
            lines.append("            inner_arr = (ctypes.c_double * inner_len)(*inner_list)")
        elif inner_element_type.name == "bool":
            lines.append("            inner_arr = (ctypes.c_bool * inner_len)(*inner_list)")

        lines.append("            inner_pair[1] = ctypes.cast(inner_arr, ctypes.c_void_p)")
        lines.append(
            f"            {param_name}_array[i] = ctypes.cast(inner_pair, ctypes.c_void_p)"
        )

        return lines

    def _generate_c_to_list_conversion(self, return_type: IRType) -> list:
        """Generate code to convert C array + length to Python list."""
        lines = []

        if not return_type.params:
            # No element type specified
            lines.append("        return result")
            return lines

        element_type = return_type.params[0]

        # Handle lists of tuples
        if element_type.kind == TypeKind.TUPLE:
            return self._generate_c_to_list_of_tuples_conversion(element_type)

        # For other complex element types, not implemented yet
        if not element_type.is_primitive():
            lines.append("        return result")
            return lines

        lines.append("        # Convert C array to Python list")
        lines.append("        if not result:")
        lines.append("            return []")
        lines.append("        # Result is [length, array_ptr] - both as void*")
        lines.append("        result_ptr = ctypes.cast(result, ctypes.POINTER(ctypes.c_void_p))")
        lines.append("        # Length is stored as intptr_t, cast back to int")
        lines.append("        list_len = ctypes.cast(result_ptr[0], ctypes.c_void_p).value or 0")
        lines.append("        if list_len == 0:")
        lines.append("            return []")
        lines.append("        array_ptr = result_ptr[1]")

        # Cast to appropriate array type and convert to Python list
        if element_type.name == "string":
            lines.append("        array = ctypes.cast(array_ptr, ctypes.POINTER(ctypes.c_char_p))")
            lines.append(
                "        python_list = [array[i].decode('utf-8') for i in range(list_len)]"
            )
            lines.append("        # Clean up C-allocated memory (strings + array + result)")
            lines.append("        _lib.ml_free_string_list_result(result)")
            lines.append("        return python_list")
        elif element_type.name == "int":
            lines.append("        array = ctypes.cast(array_ptr, ctypes.POINTER(ctypes.c_int))")
            lines.append("        python_list = [array[i] for i in range(list_len)]")
            lines.append("        # Clean up C-allocated memory")
            lines.append("        _lib.ml_free_list_result(result)")
            lines.append("        return python_list")
        elif element_type.name == "float":
            lines.append("        array = ctypes.cast(array_ptr, ctypes.POINTER(ctypes.c_double))")
            lines.append("        python_list = [array[i] for i in range(list_len)]")
            lines.append("        # Clean up C-allocated memory")
            lines.append("        _lib.ml_free_list_result(result)")
            lines.append("        return python_list")
        elif element_type.name == "bool":
            lines.append("        array = ctypes.cast(array_ptr, ctypes.POINTER(ctypes.c_bool))")
            lines.append("        python_list = [bool(array[i]) for i in range(list_len)]")
            lines.append("        # Clean up C-allocated memory")
            lines.append("        _lib.ml_free_list_result(result)")
            lines.append("        return python_list")

        return lines

    def _generate_c_to_list_of_tuples_conversion(self, tuple_type: IRType) -> list:
        """Generate code to convert C array of tuples to Python list of tuples."""
        lines = []

        if not tuple_type.params or not all(p.is_primitive() for p in tuple_type.params):
            # For complex tuple elements, not implemented yet
            lines.append("        return result")
            return lines

        lines.append("        # Convert C array of tuples to Python list")
        lines.append("        if not result:")
        lines.append("            return []")
        lines.append("        # Result is [length, tuple_array] - both as void*")
        lines.append("        result_ptr = ctypes.cast(result, ctypes.POINTER(ctypes.c_void_p))")
        lines.append("        # Length is stored as intptr_t, cast back to int")
        lines.append("        list_len = ctypes.cast(result_ptr[0], ctypes.c_void_p).value or 0")
        lines.append("        if list_len == 0:")
        lines.append("            return []")
        lines.append("        tuple_array_ptr = result_ptr[1]")
        lines.append("        # Cast to array of void** (each tuple is a void**)")
        lines.append(
            "        tuple_array = ctypes.cast(tuple_array_ptr, ctypes.POINTER(ctypes.c_void_p))"
        )
        lines.append("        ")
        lines.append("        # Convert each tuple")
        lines.append("        python_list = []")
        lines.append("        for i in range(list_len):")
        lines.append(
            "            tuple_ptr = ctypes.cast(tuple_array[i], ctypes.POINTER(ctypes.c_void_p))"
        )
        lines.append("            elements = []")

        # Convert each element of the tuple
        for j, elem_type in enumerate(tuple_type.params):
            if elem_type.name == "string":
                lines.append(
                    f"            elem_{j} = ctypes.cast(tuple_ptr[{j}], "
                    f"ctypes.c_char_p).value.decode('utf-8')"
                )
            elif elem_type.name == "int":
                lines.append(
                    f"            elem_{j} = int(ctypes.cast(tuple_ptr[{j}], "
                    f"ctypes.c_void_p).value)"
                )
            elif elem_type.name == "float":
                lines.append(
                    f"            elem_{j} = ctypes.cast(tuple_ptr[{j}], "
                    f"ctypes.POINTER(ctypes.c_double)).contents.value"
                )
            elif elem_type.name == "bool":
                lines.append(
                    f"            elem_{j} = bool(int(ctypes.cast(tuple_ptr[{j}], "
                    f"ctypes.c_void_p).value))"
                )

            lines.append(f"            elements.append(elem_{j})")

        lines.append("            python_list.append(tuple(elements))")
        lines.append("        ")
        lines.append("        # Clean up C-allocated memory")
        lines.append("        _lib.ml_free_tuple_list_result(result)")
        lines.append("        ")
        lines.append("        return python_list")

        return lines

    def _generate_tuple_to_c_conversion(self, param: "IRParameter") -> list:
        """Generate code to convert Python tuple to C array (void**)."""
        lines = []
        param_name = param.name

        if not param.type.params or not all(p.is_primitive() for p in param.type.params):
            # For complex element types, not implemented yet
            return lines

        tuple_size = len(param.type.params)

        lines.append("        # Convert Python tuple to C array")
        lines.append(f"        {param_name}_array = (ctypes.c_void_p * {tuple_size})()")

        for i, elem_type in enumerate(param.type.params):
            if elem_type.name == "string":
                lines.append(
                    f"        {param_name}_array[{i}] = ctypes.cast("
                    f"ctypes.c_char_p({param_name}[{i}].encode('utf-8')), ctypes.c_void_p)"
                )
            elif elem_type.name == "int":
                lines.append(
                    f"        {param_name}_array[{i}] = ctypes.c_void_p({param_name}[{i}])"
                )
            elif elem_type.name == "float":
                lines.append(f"        {param_name}_float_{i} = ctypes.c_double({param_name}[{i}])")
                lines.append(
                    f"        {param_name}_array[{i}] = ctypes.cast("
                    f"ctypes.pointer({param_name}_float_{i}), ctypes.c_void_p)"
                )
            elif elem_type.name == "bool":
                lines.append(
                    f"        {param_name}_array[{i}] = ctypes.c_void_p(int({param_name}[{i}]))"
                )

        return lines

    def _generate_c_to_tuple_conversion(self, return_type: IRType) -> list:
        """Generate code to convert C array (void**) to Python tuple."""
        lines = []

        if not return_type.params or not all(p.is_primitive() for p in return_type.params):
            # For complex element types, not implemented yet
            lines.append("        return result")
            return lines

        tuple_size = len(return_type.params)

        lines.append("        # Convert C array to Python tuple")
        lines.append("        if not result:")
        lines.append("            return " + str(tuple(None for _ in range(tuple_size))))
        lines.append("        result_arr = ctypes.cast(result, ctypes.POINTER(ctypes.c_void_p))")
        lines.append("        elements = []")

        for i, elem_type in enumerate(return_type.params):
            if elem_type.name == "string":
                lines.append(
                    f"        elem_{i} = ctypes.cast(result_arr[{i}], "
                    f"ctypes.c_char_p).value.decode('utf-8')"
                )
            elif elem_type.name == "int":
                lines.append(f"        elem_{i} = int(result_arr[{i}])")
            elif elem_type.name == "float":
                lines.append(
                    f"        elem_{i} = ctypes.cast(result_arr[{i}], "
                    f"ctypes.POINTER(ctypes.c_double)).contents.value"
                )
            elif elem_type.name == "bool":
                lines.append(f"        elem_{i} = bool(int(result_arr[{i}]))")

            lines.append(f"        elements.append(elem_{i})")

        lines.append("        return tuple(elements)")

        return lines

Functions

generate(module, module_name)

Generate Python wrapper module.

Source code in src/polyglot_ffi/generators/python_gen.py
def generate(self, module: IRModule, module_name: str) -> str:
    """Generate Python wrapper module."""
    # Sanitize module name for Python identifiers and filenames
    safe_name = sanitize_module_name(module_name)
    error_class = safe_name.capitalize() + "Error"

    lines = [
        "# Generated by polyglot-ffi",
        f"# {safe_name}_py.py",
        "",
        "import ctypes",
        "import sys",
        "from pathlib import Path",
        "from typing import Optional, List, Tuple, Any",
        "",
        "# Determine library extension based on platform",
        "if sys.platform == 'darwin':",
        "    _lib_ext = 'dylib'",
        "elif sys.platform == 'win32':",
        "    _lib_ext = 'dll'",
        "else:",
        "    _lib_ext = 'so'",
        "",
        "# Load the shared library",
        f'_lib_path = Path(__file__).parent / f"lib{safe_name}.{{_lib_ext}}"',
        "_lib = ctypes.CDLL(str(_lib_path))",
        "",
        "# Initialize OCaml runtime",
        "_lib.ml_init.argtypes = []",
        "_lib.ml_init.restype = None",
        "_lib.ml_init()",
        "",
        "# Configure memory cleanup functions",
        "_lib.ml_free_option.argtypes = [ctypes.c_void_p]",
        "_lib.ml_free_option.restype = None",
        "_lib.ml_free_list_result.argtypes = [ctypes.c_void_p]",
        "_lib.ml_free_list_result.restype = None",
        "_lib.ml_free_string_list_result.argtypes = [ctypes.c_void_p]",
        "_lib.ml_free_string_list_result.restype = None",
        "_lib.ml_free_tuple_list_result.argtypes = [ctypes.c_void_p]",
        "_lib.ml_free_tuple_list_result.restype = None",
        "",
        f"class {error_class}(Exception):",
        f'    """Raised when {module_name} operations fail"""',
        "    pass",
        "",
    ]

    # Configure ctypes for each function
    for func in module.functions:
        lines.append(f"# Configure {func.name}")

        # Set argtypes (filter out unit parameters - they don't appear in C signatures)
        non_unit_params = [p for p in func.params if p.type.name != "unit"]
        if non_unit_params:
            argtypes = []
            for p in non_unit_params:
                argtypes.append(self._get_ctypes(p.type))
                # For list types, add a length parameter (ctypes.c_int)
                if p.type.kind == TypeKind.LIST:
                    argtypes.append("ctypes.c_int")
            lines.append(f"_lib.ml_{func.name}.argtypes = [{', '.join(argtypes)}]")
        else:
            lines.append(f"_lib.ml_{func.name}.argtypes = []")

        # Set restype
        restype = self._get_ctypes(func.return_type)
        lines.append(f"_lib.ml_{func.name}.restype = {restype}")
        lines.append("")

    # Generate wrapper functions
    for func in module.functions:
        lines.extend(self._generate_function_wrapper(func, module_name, error_class))
        lines.append("")

    return "\n".join(lines)

Usage Example

from polyglot_ffi.generators.python_gen import PythonGenerator

generator = PythonGenerator()
wrapper = generator.generate(module, "crypto")

Path("crypto_py.py").write_text(wrapper)

Generated Output

For val encrypt : string -> string:

"""
Auto-generated Python bindings for crypto.
Generated by Polyglot FFI.
"""

import ctypes
from pathlib import Path
from typing import Optional

# Load shared library
_lib_path = Path(__file__).parent / "libcrypto.so"
_lib = ctypes.CDLL(str(_lib_path))

# Configure function signatures
_lib.ml_encrypt.argtypes = [ctypes.c_char_p]
_lib.ml_encrypt.restype = ctypes.c_char_p

def encrypt(input: str) -> str:
    """Encrypt a string."""
    result = _lib.ml_encrypt(input.encode('utf-8'))
    if result is None:
        raise RuntimeError("encrypt returned NULL")
    return result.decode('utf-8')

Dune Generator

Generates Dune build system configuration.

polyglot_ffi.generators.dune_gen.DuneGenerator

Generate Dune build configuration files.

Source code in src/polyglot_ffi/generators/dune_gen.py
class DuneGenerator:
    """Generate Dune build configuration files."""

    def _format_packages(self, ocaml_libraries: Optional[List[str]] = None) -> str:
        """Format additional packages for ocamlfind -package flag."""
        if not ocaml_libraries:
            return ""
        return "," + ",".join(ocaml_libraries)

    def _get_library_link_flags(self, ocaml_libraries: Optional[List[str]] = None) -> str:
        """Get linker flags for OCaml libraries with C dependencies."""
        if not ocaml_libraries:
            return ""

        # Map OCaml library names to their C library flags
        # Note: The C library names often differ from OCaml package names
        lib_flags_map = {
            "str": "-lcamlstr",  # OCaml Str module -> libcamlstr
            "unix": "-lunix",
            "threads": "-lthreadsnat",
        }

        flags = []
        for lib in ocaml_libraries:
            if lib in lib_flags_map:
                flags.append(lib_flags_map[lib])

        return " " + " ".join(flags) if flags else ""

    def generate_dune(self, module_name: str, ocaml_libraries: Optional[List[str]] = None) -> str:
        """Generate dune file for the bindings library.

        Args:
            module_name: Name of the module
            ocaml_libraries: Additional OCaml libraries to link (e.g., ['str', 'unix'])
        """
        safe_name = sanitize_module_name(module_name)

        # Build libraries list
        # Note: We only include 'ctypes' here. ctypes.foreign is used via -package in ocamlfind commands
        # but doesn't need to be in the library list (it shares the same directory as ctypes)
        libs = ["ctypes"]
        if ocaml_libraries:
            libs.extend(ocaml_libraries)
        libs_str = " ".join(libs)

        return f"""; Generated by polyglot-ffi
(library
 (name {safe_name}_bindings)
 (public_name {safe_name}_bindings)
 (libraries {libs_str})
 (ctypes
  (external_library_name {safe_name})
  (build_flags_resolver
   (vendored (c_flags :standard) (c_library_flags :standard)))
  (headers (preamble "#include \\"{safe_name}_stubs.h\\""))
  (type_description
   (instance Type)
   (functor Type_description))
  (function_description
   (concurrency sequential)
   (instance Function)
   (functor Function_description))
  (generated_types Types_generated)
  (generated_entry_point C)))

; Compile C stubs to object file
(rule
 (targets {safe_name}_stubs.o)
 (deps {safe_name}_stubs.c {safe_name}_stubs.h)
 (action
  (run gcc -c -I%{{ocaml_where}} -fPIC {safe_name}_stubs.c -o {safe_name}_stubs.o)))

; Build OCaml object file for standalone shared library
(rule
 (targets lib{safe_name}_ocaml.o)
 (deps {safe_name}.ml {safe_name}.mli)
 (action
  (progn
   (run ocamlfind ocamlopt -c -thread -package ctypes.foreign{self._format_packages(ocaml_libraries)} {safe_name}.mli)
   (run ocamlfind ocamlopt -thread -output-obj -o lib{safe_name}_ocaml.o
    -package ctypes.foreign{self._format_packages(ocaml_libraries)} -linkpkg
    {safe_name}.ml))))

; Create .dylib from object files on macOS
(rule
 (targets lib{safe_name}.dylib)
 (deps lib{safe_name}_ocaml.o {safe_name}_stubs.o)
 (enabled_if (= %{{system}} macosx))
 (mode promote)
 (action
  (run gcc -dynamiclib -o lib{safe_name}.dylib
   lib{safe_name}_ocaml.o {safe_name}_stubs.o
   -L%{{ocaml_where}} -lasmrun -lunix -lthreadsnat{self._get_library_link_flags(ocaml_libraries)})))

; Create .so from object files on Linux
(rule
 (targets lib{safe_name}.so)
 (deps lib{safe_name}_ocaml.o {safe_name}_stubs.o)
 (enabled_if (= %{{system}} linux))
 (mode promote)
 (action
  (run gcc -shared -o lib{safe_name}.so
   lib{safe_name}_ocaml.o {safe_name}_stubs.o
   -L%{{ocaml_where}} -lasmrun -lunix -lthreadsnat{self._get_library_link_flags(ocaml_libraries)})))
"""

    def generate_dune_project(self, module_name: str) -> str:
        """Generate dune-project file."""
        safe_name = sanitize_module_name(module_name)
        return f"""(lang dune 3.16)
(using ctypes 0.3)

; Generated by polyglot-ffi

(name {safe_name}_bindings)

(generate_opam_files true)

(package
 (name {safe_name}_bindings)
 (synopsis "OCaml-Python bindings for {module_name}")
 (description "Auto-generated FFI bindings by polyglot-ffi")
 (depends
  (ocaml (>= 4.14))
  (dune (>= 3.16))
  (ctypes (>= 0.20.0))
  (ctypes-foreign (>= 0.20.0))))
"""

Functions

generate_dune(module_name, ocaml_libraries=None)

Generate dune file for the bindings library.

Parameters:

Name Type Description Default
module_name str

Name of the module

required
ocaml_libraries Optional[List[str]]

Additional OCaml libraries to link (e.g., ['str', 'unix'])

None
Source code in src/polyglot_ffi/generators/dune_gen.py
    def generate_dune(self, module_name: str, ocaml_libraries: Optional[List[str]] = None) -> str:
        """Generate dune file for the bindings library.

        Args:
            module_name: Name of the module
            ocaml_libraries: Additional OCaml libraries to link (e.g., ['str', 'unix'])
        """
        safe_name = sanitize_module_name(module_name)

        # Build libraries list
        # Note: We only include 'ctypes' here. ctypes.foreign is used via -package in ocamlfind commands
        # but doesn't need to be in the library list (it shares the same directory as ctypes)
        libs = ["ctypes"]
        if ocaml_libraries:
            libs.extend(ocaml_libraries)
        libs_str = " ".join(libs)

        return f"""; Generated by polyglot-ffi
(library
 (name {safe_name}_bindings)
 (public_name {safe_name}_bindings)
 (libraries {libs_str})
 (ctypes
  (external_library_name {safe_name})
  (build_flags_resolver
   (vendored (c_flags :standard) (c_library_flags :standard)))
  (headers (preamble "#include \\"{safe_name}_stubs.h\\""))
  (type_description
   (instance Type)
   (functor Type_description))
  (function_description
   (concurrency sequential)
   (instance Function)
   (functor Function_description))
  (generated_types Types_generated)
  (generated_entry_point C)))

; Compile C stubs to object file
(rule
 (targets {safe_name}_stubs.o)
 (deps {safe_name}_stubs.c {safe_name}_stubs.h)
 (action
  (run gcc -c -I%{{ocaml_where}} -fPIC {safe_name}_stubs.c -o {safe_name}_stubs.o)))

; Build OCaml object file for standalone shared library
(rule
 (targets lib{safe_name}_ocaml.o)
 (deps {safe_name}.ml {safe_name}.mli)
 (action
  (progn
   (run ocamlfind ocamlopt -c -thread -package ctypes.foreign{self._format_packages(ocaml_libraries)} {safe_name}.mli)
   (run ocamlfind ocamlopt -thread -output-obj -o lib{safe_name}_ocaml.o
    -package ctypes.foreign{self._format_packages(ocaml_libraries)} -linkpkg
    {safe_name}.ml))))

; Create .dylib from object files on macOS
(rule
 (targets lib{safe_name}.dylib)
 (deps lib{safe_name}_ocaml.o {safe_name}_stubs.o)
 (enabled_if (= %{{system}} macosx))
 (mode promote)
 (action
  (run gcc -dynamiclib -o lib{safe_name}.dylib
   lib{safe_name}_ocaml.o {safe_name}_stubs.o
   -L%{{ocaml_where}} -lasmrun -lunix -lthreadsnat{self._get_library_link_flags(ocaml_libraries)})))

; Create .so from object files on Linux
(rule
 (targets lib{safe_name}.so)
 (deps lib{safe_name}_ocaml.o {safe_name}_stubs.o)
 (enabled_if (= %{{system}} linux))
 (mode promote)
 (action
  (run gcc -shared -o lib{safe_name}.so
   lib{safe_name}_ocaml.o {safe_name}_stubs.o
   -L%{{ocaml_where}} -lasmrun -lunix -lthreadsnat{self._get_library_link_flags(ocaml_libraries)})))
"""
generate_dune_project(module_name)

Generate dune-project file.

Source code in src/polyglot_ffi/generators/dune_gen.py
    def generate_dune_project(self, module_name: str) -> str:
        """Generate dune-project file."""
        safe_name = sanitize_module_name(module_name)
        return f"""(lang dune 3.16)
(using ctypes 0.3)

; Generated by polyglot-ffi

(name {safe_name}_bindings)

(generate_opam_files true)

(package
 (name {safe_name}_bindings)
 (synopsis "OCaml-Python bindings for {module_name}")
 (description "Auto-generated FFI bindings by polyglot-ffi")
 (depends
  (ocaml (>= 4.14))
  (dune (>= 3.16))
  (ctypes (>= 0.20.0))
  (ctypes-foreign (>= 0.20.0))))
"""

Usage Example

from polyglot_ffi.generators.dune_gen import DuneGenerator

generator = DuneGenerator()
dune = generator.generate_dune("crypto")
dune_project = generator.generate_dune_project("crypto")

Path("dune").write_text(dune)
Path("dune-project").write_text(dune_project)

Generated Output

; dune
(library
 (name crypto)
 (libraries ctypes ctypes.foreign)
 (foreign_stubs
  (language c)
  (names stubs)))

; dune-project
(lang dune 3.0)
(name crypto)

Complete Generation Workflow

from pathlib import Path
from polyglot_ffi.parsers.ocaml import parse_mli_file
from polyglot_ffi.generators.ctypes_gen import CtypesGenerator
from polyglot_ffi.generators.c_stubs_gen import CStubGenerator
from polyglot_ffi.generators.python_gen import PythonGenerator
from polyglot_ffi.generators.dune_gen import DuneGenerator

# Parse
module = parse_mli_file(Path("crypto.mli"))

# Generate all artifacts
output_dir = Path("generated")
output_dir.mkdir(exist_ok=True)

# OCaml ctypes
ctypes_gen = CtypesGenerator()
(output_dir / "type_description.ml").write_text(
    ctypes_gen.generate_type_description(module)
)
(output_dir / "function_description.ml").write_text(
    ctypes_gen.generate_function_description(module)
)

# C stubs
c_gen = CStubGenerator()
(output_dir / "stubs.c").write_text(
    c_gen.generate_stubs(module, "crypto")
)
(output_dir / "stubs.h").write_text(
    c_gen.generate_header(module, "crypto")
)

# Python wrapper
py_gen = PythonGenerator()
(output_dir / "crypto_py.py").write_text(
    py_gen.generate(module, "crypto")
)

# Build system
dune_gen = DuneGenerator()
(output_dir / "dune").write_text(
    dune_gen.generate_dune("crypto")
)
(output_dir / "dune-project").write_text(
    dune_gen.generate_dune_project("crypto")
)

print(f"Generated {len(module.functions)} function bindings")

Type Mapping

All generators use the type registry for consistent type mappings:

from polyglot_ffi.type_system.registry import get_default_registry
from polyglot_ffi.ir.types import STRING, INT, ir_option

registry = get_default_registry()

# Primitive types
registry.get_mapping(STRING, "python")  # "str"
registry.get_mapping(INT, "python")     # "int"

# Complex types
opt_string = ir_option(STRING)
registry.get_mapping(opt_string, "python")  # "Optional[str]"

Adding Custom Generators

To add support for a new target language:

from polyglot_ffi.ir.types import IRModule

class RustGenerator:
    """Generate Rust FFI bindings."""

    def generate(self, module: IRModule, module_name: str) -> str:
        """Generate Rust code from IR."""
        lines = [
            f"// Auto-generated Rust bindings for {module_name}",
            "",
            "use std::ffi::CString;",
            "use std::os::raw::c_char;",
            "",
        ]

        for func in module.functions:
            # Generate extern declaration
            lines.append(f"extern \"C\" {{")
            lines.append(f"    fn ml_{func.name}(...) -> ...;")
            lines.append("}")

            # Generate safe wrapper
            lines.append(f"pub fn {func.name}(...) -> ... {{")
            lines.append(f"    unsafe {{ ml_{func.name}(...) }}")
            lines.append("}")
            lines.append("")

        return "\n".join(lines)

Performance

Generators are highly optimized:

  • Ctypes: ~0.0001ms per generation (extremely fast)
  • C Stubs: ~0.009ms per generation
  • Python: ~0.008ms per generation
  • Complete workflow: ~0.07ms for 6 functions

All generators can be run in parallel for additional speedup.

See Also