├── .gitignore ├── deno.json ├── lib ├── renderers │ ├── Type.ts │ ├── Var.ts │ ├── Union.ts │ ├── Function.ts │ ├── Enum.ts │ ├── Type │ │ ├── asTS.ts │ │ ├── asFfiBindings.ts │ │ └── asFfiBindingTypes.ts │ ├── ClassTemplate.ts │ └── Typedef.ts ├── visitors │ ├── Var.ts │ ├── Enum.ts │ ├── Typedef.ts │ ├── Union.ts │ ├── Function.ts │ ├── Class.ts │ ├── ClassTemplate.ts │ └── Type.ts ├── renderer.ts ├── types.d.ts ├── mod.ts ├── utils.ts └── Context.ts ├── tests ├── output │ ├── ffi.ts │ ├── systemClasses.ts │ ├── std_function.h.ts │ ├── systemTypes.ts │ ├── std_function.h.types.ts │ └── std_function.h.classes.ts ├── std_function.h ├── std_function.test.ts └── named_sem.hpp ├── examples └── basic_usage.ts ├── LICENSE ├── deno.lock └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": { 3 | "include": ["tests"] 4 | }, 5 | "tasks": { 6 | "test": "LIBCLANG_PATH=/usr/lib deno test --unstable -A" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/renderers/Type.ts: -------------------------------------------------------------------------------- 1 | export { renderTypeAsFfiBindingTypes } from "./Type/asFfiBindingTypes.ts"; 2 | export { 3 | renderTypeAsFfiBindings, 4 | renderTypeAsFfiBindings as renderTypeAsFfi, 5 | } from "./Type/asFfiBindings.ts"; 6 | export { renderTypeAsTS } from "./Type/asTS.ts"; 7 | -------------------------------------------------------------------------------- /tests/output/ffi.ts: -------------------------------------------------------------------------------- 1 | import * as STD_FUNCTION from "./std_function.h.ts"; 2 | 3 | const lib = Deno.dlopen("FFIPATH", { 4 | ...STD_FUNCTION, 5 | }); 6 | 7 | export const MyClass__Constructor = lib.symbols.MyClass__Constructor; 8 | export const PodClass__create = lib.symbols.PodClass__create; 9 | export const tryFunction = lib.symbols.tryFunction; 10 | -------------------------------------------------------------------------------- /lib/visitors/Var.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "../Context.ts"; 2 | import { VarEntry } from "../types.d.ts"; 3 | import { visitType } from "./Type.ts"; 4 | 5 | export const visitVarEntry = (context: Context, entry: VarEntry): void => { 6 | const type = visitType(context, entry.cursor.getType()!); 7 | 8 | entry.type = type; 9 | entry.used = true; 10 | }; 11 | -------------------------------------------------------------------------------- /lib/renderers/Var.ts: -------------------------------------------------------------------------------- 1 | import { SEP } from "../Context.ts"; 2 | import { RenderData, VarEntry } from "../types.d.ts"; 3 | import { createDummyRenderDataEntry } from "../utils.ts"; 4 | import { renderTypeAsFfi } from "./Type.ts"; 5 | 6 | export const renderVar = ({ 7 | entriesInBindingsFile, 8 | importsInBindingsFile, 9 | }: RenderData, entry: VarEntry) => 10 | void entriesInBindingsFile.push(createDummyRenderDataEntry( 11 | `export const ${entry.nsName.replaceAll(SEP, "__")} = { 12 | name: "${entry.mangling}", 13 | type: ${renderTypeAsFfi(new Set(), importsInBindingsFile, entry.type)}, 14 | } as const; 15 | `, 16 | )); 17 | -------------------------------------------------------------------------------- /tests/output/systemClasses.ts: -------------------------------------------------------------------------------- 1 | import { FUNCTION_BASE_SIZE } from "./systemTypes.ts"; 2 | 3 | export class _Function_baseBuffer extends Uint8Array { 4 | constructor(arg?: ArrayBufferLike | number) { 5 | if (typeof arg === "undefined") { 6 | super(FUNCTION_BASE_SIZE); 7 | return; 8 | } else if (typeof arg === "number") { 9 | if (!Number.isFinite(arg) || arg < FUNCTION_BASE_SIZE) { 10 | throw new Error( 11 | "Invalid construction of _Function_baseBuffer: Size is not finite or is too small", 12 | ); 13 | } 14 | super(arg); 15 | return; 16 | } 17 | if (arg.byteLength < FUNCTION_BASE_SIZE) { 18 | throw new Error( 19 | "Invalid construction of _Function_baseBuffer: Buffer size is too small", 20 | ); 21 | } 22 | super(arg); 23 | } 24 | } 25 | 26 | export class functionBuffer<_Signature extends Deno.UnsafeCallbackDefinition> 27 | extends Uint8Array {} 28 | -------------------------------------------------------------------------------- /lib/visitors/Enum.ts: -------------------------------------------------------------------------------- 1 | import { Context } from "../Context.ts"; 2 | import { EnumEntry } from "../types.d.ts"; 3 | import { visitType } from "./Type.ts"; 4 | 5 | export const visitEnum = (context: Context, name: string): EnumEntry => { 6 | const enums = context.getEnums(); 7 | const found = enums.find((entry) => entry.nsName === name) || 8 | enums.find((entry) => entry.name === name); 9 | if (!found) { 10 | throw new Error(`Could not find enum '${name}'`); 11 | } 12 | found.used = true; 13 | if (found.type === null) { 14 | const integerType = found.cursor.getEnumDeclarationIntegerType(); 15 | if (!integerType) { 16 | throw new Error(`Could not find integer type for enum '${name}'`); 17 | } 18 | const result = visitType(context, integerType); 19 | if (result === null) { 20 | throw new Error(`Found void integer value for enum '${name}'`); 21 | } 22 | found.type = result; 23 | } 24 | return found; 25 | }; 26 | -------------------------------------------------------------------------------- /examples/basic_usage.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S deno run --unstable --allow-env=LIBCLANG_PATH --allow-ffi --allow-write 2 | 3 | import { build } from "../lib/mod.ts"; 4 | 5 | const LIB_BASE_PATH = "/path/to/sources/"; 6 | 7 | build({ 8 | basePath: LIB_BASE_PATH, 9 | outputPath: "/path/to/output", 10 | files: [ 11 | `${LIB_BASE_PATH}some/header.h`, 12 | `${LIB_BASE_PATH}another/deeper/header.h`, 13 | ], 14 | imports: [ 15 | { 16 | kind: "class", 17 | name: "DataClass", 18 | constructors: true, 19 | destructors: true, 20 | methods: false, 21 | }, 22 | { 23 | kind: "class", 24 | name: "MyClass", 25 | constructors: true, 26 | destructors: true, 27 | methods: [ 28 | "methodA", 29 | "methodB", 30 | ], 31 | }, 32 | ], 33 | include: [ 34 | `${LIB_BASE_PATH}some`, 35 | `${LIB_BASE_PATH}another`, 36 | "/lib64/clang/14.0.6/include", 37 | ], 38 | }); 39 | -------------------------------------------------------------------------------- /tests/std_function.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | typedef std::function NullaryCallback; 4 | typedef std::function UnaryCallback; 5 | typedef std::function BinaryCallback; 6 | 7 | class MyClass { 8 | public: 9 | typedef std::function TernaryCallback; 10 | MyClass(); 11 | 12 | private: 13 | NullaryCallback a_; 14 | UnaryCallback b_; 15 | BinaryCallback c_; 16 | TernaryCallback d_; 17 | }; 18 | 19 | class PodClass { 20 | public: 21 | static PodClass* create(); 22 | 23 | private: 24 | int data_; 25 | }; 26 | 27 | class OtherPodClass { 28 | int data_{6}; 29 | }; 30 | 31 | class NonPodClass { 32 | private: 33 | int data_; 34 | ~NonPodClass(); 35 | }; 36 | 37 | typedef void (*ClassCallback)(OtherPodClass, NonPodClass, NonPodClass&); 38 | 39 | void tryFunction(ClassCallback cb, PodClass, PodClass&, OtherPodClass, OtherPodClass&, NonPodClass, NonPodClass&); 40 | 41 | static int kValue = 3; -------------------------------------------------------------------------------- /tests/output/std_function.h.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ClassCallbackT, 3 | MyClassT, 4 | NonPodClassT, 5 | OtherPodClassT, 6 | PodClassT, 7 | } from "./std_function.h.types.ts"; 8 | import { buf, func, ptr } from "./systemTypes.ts"; 9 | 10 | export const MyClass__Constructor = { 11 | name: "_ZN7MyClassC1Ev", 12 | parameters: [buf(MyClassT)], 13 | result: "void", 14 | } as const; 15 | 16 | export const PodClass__create = { 17 | name: "_ZN8PodClass6createEv", 18 | parameters: [], 19 | result: ptr(PodClassT), 20 | } as const; 21 | 22 | export const tryFunction = { 23 | name: 24 | "_Z11tryFunctionPFv13OtherPodClass11NonPodClassRS0_E8PodClassRS4_S_RS_S0_S1_", 25 | parameters: [ 26 | func(ClassCallbackT), 27 | PodClassT, 28 | ptr(PodClassT), 29 | OtherPodClassT, 30 | buf(OtherPodClassT), 31 | buf(NonPodClassT), 32 | buf(NonPodClassT), 33 | ], 34 | result: "void", 35 | } as const; 36 | 37 | export const kValue = { 38 | name: "_ZL6kValue", 39 | type: "i32", 40 | } as const; 41 | -------------------------------------------------------------------------------- /lib/renderers/Union.ts: -------------------------------------------------------------------------------- 1 | import { RenderData, UnionEntry } from "../types.d.ts"; 2 | import { 3 | createRenderDataEntry, 4 | getSizeOfType, 5 | SYSTEM_TYPES, 6 | } from "../utils.ts"; 7 | import { renderTypeAsFfi } from "./Type.ts"; 8 | 9 | export const renderUnion = ( 10 | { 11 | entriesInTypesFile, 12 | importsInTypesFile, 13 | }: RenderData, 14 | entry: UnionEntry, 15 | ) => { 16 | const dependencies = new Set(); 17 | const nameT = `${entry.name}T`; 18 | 19 | const uniqueSortedFields = new Set( 20 | entry.fields.sort((a, b) => getSizeOfType(b) - getSizeOfType(a)).map( 21 | (field) => renderTypeAsFfi(dependencies, importsInTypesFile, field), 22 | ), 23 | ); 24 | importsInTypesFile.set(`union${uniqueSortedFields.size}`, SYSTEM_TYPES); 25 | const typesEntry = `export const ${nameT} = union${uniqueSortedFields.size}( 26 | ${[...uniqueSortedFields].join(", ")} 27 | )`; 28 | 29 | entriesInTypesFile.push( 30 | createRenderDataEntry([nameT], [...dependencies], typesEntry), 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /lib/visitors/Typedef.ts: -------------------------------------------------------------------------------- 1 | import { CXCursor } from "https://deno.land/x/libclang@1.0.0-beta.8/mod.ts"; 2 | import { Context } from "../Context.ts"; 3 | import { TypedefEntry } from "../types.d.ts"; 4 | import { visitType } from "./Type.ts"; 5 | 6 | export const visitTypedefEntry = ( 7 | context: Context, 8 | cursor: CXCursor, 9 | ): TypedefEntry => { 10 | const typedefs = context.getTypedefs(); 11 | const found = typedefs.find((entry) => entry.cursor.equals(cursor)); 12 | if (!found) { 13 | throw new Error( 14 | `Could not find typedef by cursor '${cursor.getSpelling()}'`, 15 | ); 16 | } 17 | found.used = true; 18 | if (found.target === null) { 19 | const referredType = found.cursor 20 | .getTypedefDeclarationOfUnderlyingType(); 21 | if (!referredType) { 22 | throw new Error( 23 | `Could not find referred type for typedef '${found.nsName}'`, 24 | ); 25 | } 26 | const result = visitType(context, referredType); 27 | found.target = result; 28 | } 29 | return found; 30 | }; 31 | -------------------------------------------------------------------------------- /lib/visitors/Union.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CXChildVisitResult, 3 | CXCursorKind, 4 | } from "https://deno.land/x/libclang@1.0.0-beta.8/include/typeDefinitions.ts"; 5 | import { CXCursor } from "https://deno.land/x/libclang@1.0.0-beta.8/mod.ts"; 6 | import { Context } from "../Context.ts"; 7 | import { visitType } from "./Type.ts"; 8 | 9 | export const visitUnionCursor = (context: Context, cursor: CXCursor) => { 10 | const entry = context.findUnionByCursor(cursor); 11 | if (!entry) { 12 | throw new Error("Could not find union"); 13 | } 14 | 15 | if (entry.used) { 16 | return entry; 17 | } 18 | 19 | entry.cursor.visitChildren((child) => { 20 | if (child.kind === CXCursorKind.CXCursor_FieldDecl) { 21 | const typeEntry = visitType(context, child.getType()!); 22 | if (!typeEntry) { 23 | throw new Error("Failed to visit union field"); 24 | } 25 | entry.fields.push(typeEntry); 26 | } 27 | return CXChildVisitResult.CXChildVisit_Continue; 28 | }); 29 | 30 | entry.used = true; 31 | 32 | return entry; 33 | }; 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Aapo Alasuutari 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/std_function.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | dirname, 3 | fromFileUrl, 4 | resolve, 5 | } from "https://deno.land/std@0.170.0/path/mod.ts"; 6 | import { build } from "../lib/mod.ts"; 7 | import { AbsoluteFilePath } from "../lib/types.d.ts"; 8 | 9 | const TESTS_BASE_PATH = dirname( 10 | fromFileUrl(import.meta.url), 11 | ) as AbsoluteFilePath; 12 | const OUTPUT_PATH = resolve(TESTS_BASE_PATH, "output") as AbsoluteFilePath; 13 | 14 | Deno.test("std::function", async (t) => { 15 | Deno.mkdirSync(OUTPUT_PATH, { recursive: true }); 16 | build({ 17 | basePath: TESTS_BASE_PATH, 18 | files: [`${TESTS_BASE_PATH}/std_function.h`], 19 | imports: [{ 20 | kind: "class", 21 | constructors: true, 22 | destructors: true, 23 | methods: true, 24 | name: "MyClass", 25 | }, { 26 | kind: "var", 27 | name: "kValue", 28 | }, { 29 | kind: "class", 30 | constructors: true, 31 | destructors: true, 32 | methods: true, 33 | name: "PodClass", 34 | }, { 35 | kind: "class", 36 | constructors: true, 37 | destructors: true, 38 | methods: true, 39 | name: "OtherPodClass", 40 | }, { 41 | kind: "class", 42 | constructors: true, 43 | destructors: true, 44 | methods: true, 45 | name: "NonPodClass", 46 | }, { 47 | kind: "function", 48 | name: "tryFunction", 49 | }], 50 | include: [TESTS_BASE_PATH, "/lib64/clang/15.0.7/include"], 51 | outputPath: OUTPUT_PATH, 52 | }); 53 | await t.step("nullary", async (_t) => { 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /tests/named_sem.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * I Made a POSIX Semaphore Wrapper and I'm So Sorry 3 | * 4 | * by imaami: https://gist.github.com/imaami/4744a322a53d6765124c472193b22067 5 | * 6 | * Compile with -std=c++20 or later. Example of use: 7 | * 8 | * ::sem_t *sem = new named_sem<"/when_you_see_it", 9 | * O_CREAT | O_RDWR, 10 | * 0666, 0>(); 11 | * if (sem) { 12 | * sem->post(); 13 | * delete sem; 14 | * } 15 | */ 16 | #ifndef NAMED_SEM_HPP_ 17 | #define NAMED_SEM_HPP_ 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | template 26 | struct fixed_str 27 | { 28 | char buf[N]{}; 29 | constexpr fixed_str(char const (&s)[N]) noexcept { 30 | std::copy_n(s, N, buf); 31 | } 32 | }; 33 | 34 | template 35 | fixed_str(char const (&)[N]) -> fixed_str; 36 | 37 | template 38 | class named_sem 39 | { 40 | [[maybe_unused]] ::sem_t d_; 41 | ::sem_t *evil() { return reinterpret_cast<::sem_t *>(this); } 42 | 43 | public: 44 | named_sem() noexcept {} 45 | ~named_sem() noexcept {} 46 | 47 | int getvalue(int *v) noexcept { return ::sem_getvalue(evil(), v); } 48 | int post() noexcept { return ::sem_post(evil()); } 49 | int wait() noexcept { return ::sem_wait(evil()); } 50 | int trywait() noexcept { return ::sem_trywait(evil()); } 51 | int timedwait(const struct timespec *t) noexcept { return ::sem_timedwait(evil(), t); } 52 | 53 | static void *operator new(std::size_t) noexcept { 54 | ::sem_t *s = ::sem_open(name.buf, oflag, mode, value); 55 | return s != SEM_FAILED ? static_cast(s) : nullptr; 56 | } 57 | 58 | static void operator delete(void *ptr) noexcept { 59 | ::sem_close(static_cast<::sem_t *>(ptr)); 60 | } 61 | 62 | explicit named_sem(named_sem const &) noexcept = delete; 63 | explicit named_sem(named_sem const &&) noexcept = delete; 64 | named_sem &operator=(named_sem const &&) noexcept = delete; 65 | named_sem &operator=(named_sem const &) noexcept = delete; 66 | }; 67 | 68 | #endif // NAMED_SEM_HPP_ -------------------------------------------------------------------------------- /tests/output/systemTypes.ts: -------------------------------------------------------------------------------- 1 | export const union3 = ( 2 | a: T, 3 | _b: U, 4 | _c: V, 5 | ): T | U | V => a; 6 | 7 | export const union2 = (a: T, _b: U): T | U => a; 8 | 9 | declare const PtrBrand: unique symbol; 10 | export type Ptr = "pointer" & { [PtrBrand]: T }; 11 | export const ptr = (_: T) => "pointer" as Ptr; 12 | 13 | declare const FuncBrand: unique symbol; 14 | export type Func = "function" & { [FuncBrand]: T }; 15 | export const func = (_: T) => "function" as Func; 16 | 17 | declare const BufBrand: unique symbol; 18 | export type Buf = "buffer" & { [BufBrand]: T }; 19 | export const buf = (_: T) => "buffer" as Buf; 20 | 21 | export const _Nocopy_typesT = union3( 22 | { "struct": ["u64", "u64"] }, 23 | "pointer", 24 | func({ parameters: [], result: "void" }), 25 | ); 26 | export const _Any_dataT = union2( 27 | _Nocopy_typesT, 28 | { 29 | struct: [ 30 | "i8", 31 | "i8", 32 | "i8", 33 | "i8", 34 | "i8", 35 | "i8", 36 | "i8", 37 | "i8", 38 | "i8", 39 | "i8", 40 | "i8", 41 | "i8", 42 | "i8", 43 | "i8", 44 | "i8", 45 | "i8", 46 | ], 47 | }, 48 | ); 49 | export const _Manager_operationT = "u32"; 50 | export const enum _Manager_operation { 51 | __get_type_info, 52 | __get_functor_ptr, 53 | __clone_functor, 54 | __destroy_functor, 55 | } 56 | 57 | export const _Manager_typeT = { 58 | parameters: [ptr(_Any_dataT), ptr(_Any_dataT), _Manager_operationT], 59 | result: "bool", 60 | } as const; 61 | declare const _Manager_type_: unique symbol; 62 | export type _Manager_type = NonNullable & { 63 | [_Manager_type_]: unknown; 64 | }; 65 | 66 | export const FUNCTION_BASE_SIZE = 24 as const; 67 | export const _Function_baseT = { 68 | struct: [ 69 | _Any_dataT, // _M_functor, offset 0, size 16, align 8 70 | func(_Manager_typeT), // _M_manager, offset 16, size 8, align 8 71 | ], 72 | } as const; 73 | declare const _Function_base: unique symbol; 74 | export type _Function_basePointer = NonNullable & { 75 | [_Function_base]: unknown; 76 | }; 77 | 78 | declare const functionTemplate: unique symbol; 79 | declare const function__Signature: unique symbol; 80 | export type functionPointer = 81 | & _Function_basePointer 82 | & { [functionTemplate]: unknown; [function__Signature]: Signature }; 83 | export const functionT = < 84 | const Signature extends Deno.UnsafeCallbackDefinition, 85 | >( 86 | signature: Signature, 87 | ) => { 88 | const { parameters: argTypes, result: res } = signature; 89 | return { 90 | struct: [ 91 | _Function_baseT, // base class, size 24, align 8 92 | func({ 93 | parameters: [ptr(_Any_dataT), ...argTypes.map(ptr)], 94 | result: res, 95 | }), // _M_invoker 96 | ], 97 | } as const; 98 | }; 99 | -------------------------------------------------------------------------------- /tests/output/std_function.h.types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type Buf, 3 | buf, 4 | type functionPointer, 5 | functionT, 6 | } from "./systemTypes.ts"; 7 | 8 | export const NULLARY_CALLBACK_SIZE = 32 as const; 9 | export const NullaryCallbackT = functionT({ parameters: [], result: "void" }); 10 | export type NullaryCallbackPointer = functionPointer< 11 | Deno.UnsafeCallbackDefinition<[], "void"> 12 | >; 13 | 14 | export const UNARY_CALLBACK_SIZE = 32 as const; 15 | export const UnaryCallbackT = functionT({ 16 | parameters: ["i32"], 17 | result: "void", 18 | }); 19 | export type UnaryCallbackPointer = functionPointer< 20 | Deno.UnsafeCallbackDefinition<["i32"], "void"> 21 | >; 22 | 23 | export const BINARY_CALLBACK_SIZE = 32 as const; 24 | export const BinaryCallbackT = functionT({ 25 | parameters: ["i32", "i32"], 26 | result: "void", 27 | }); 28 | export type BinaryCallbackPointer = functionPointer< 29 | Deno.UnsafeCallbackDefinition<["i32", "i32"], "void"> 30 | >; 31 | 32 | export const TERNARY_CALLBACK_SIZE = 32 as const; 33 | export const TernaryCallbackT = functionT({ 34 | parameters: ["i32", "i32", buf("self")], 35 | result: "void", 36 | }); 37 | export type TernaryCallbackPointer = functionPointer< 38 | Deno.UnsafeCallbackDefinition<["i32", "i32", Buf<"self">], "void"> 39 | >; 40 | 41 | export const MY_CLASS_SIZE = 128 as const; 42 | export const MyClassT = { 43 | struct: [ 44 | NullaryCallbackT, // a_, offset 0, size 32, align 8 45 | UnaryCallbackT, // b_, offset 32, size 32, align 8 46 | BinaryCallbackT, // c_, offset 64, size 32, align 8 47 | TernaryCallbackT, // d_, offset 96, size 32, align 8 48 | ], 49 | } as const; 50 | declare const MyClass: unique symbol; 51 | export type MyClassPointer = NonNullable & { 52 | [MyClass]: unknown; 53 | }; 54 | 55 | export const POD_CLASS_SIZE = 4 as const; 56 | export const PodClassT = { 57 | struct: [ 58 | "i32", // data_, offset 0, size 4, align 4 59 | ], 60 | } as const; 61 | declare const PodClass: unique symbol; 62 | export type PodClassPointer = NonNullable & { 63 | [PodClass]: unknown; 64 | }; 65 | 66 | export const OTHER_POD_CLASS_SIZE = 4 as const; 67 | export const OtherPodClassT = { 68 | struct: [ 69 | "i32", // data_, offset 0, size 4, align 4 70 | ], 71 | } as const; 72 | declare const OtherPodClass: unique symbol; 73 | export type OtherPodClassPointer = NonNullable & { 74 | [OtherPodClass]: unknown; 75 | }; 76 | 77 | export const NON_POD_CLASS_SIZE = 4 as const; 78 | export const NonPodClassT = { 79 | struct: [ 80 | "i32", // data_, offset 0, size 4, align 4 81 | ], 82 | } as const; 83 | declare const NonPodClass: unique symbol; 84 | export type NonPodClassPointer = NonNullable & { 85 | [NonPodClass]: unknown; 86 | }; 87 | 88 | export const ClassCallbackT = { 89 | parameters: [OtherPodClassT, NonPodClassT, buf(NonPodClassT)], 90 | result: "void", 91 | } as const; 92 | declare const ClassCallback_: unique symbol; 93 | export type ClassCallback = NonNullable & { 94 | [ClassCallback_]: unknown; 95 | }; 96 | -------------------------------------------------------------------------------- /lib/renderers/Function.ts: -------------------------------------------------------------------------------- 1 | import { SEP } from "../Context.ts"; 2 | import { 3 | FunctionEntry, 4 | ImportMap, 5 | Parameter, 6 | RenderData, 7 | TypeEntry, 8 | } from "../types.d.ts"; 9 | import { 10 | createDummyRenderDataEntry, 11 | isInlineTemplateStruct, 12 | isPassableByValue, 13 | isStruct, 14 | SYSTEM_TYPES, 15 | } from "../utils.ts"; 16 | import { renderTypeAsFfi } from "./Type.ts"; 17 | 18 | export const renderFunction = ( 19 | { bindings, entriesInBindingsFile, importsInBindingsFile }: RenderData, 20 | entry: FunctionEntry, 21 | ) => { 22 | const dependencies = new Set(); 23 | const namespace__method = entry.nsName.replaceAll(SEP, "__"); 24 | bindings.add(namespace__method); 25 | const parameterStrings = entry.parameters.map((param) => 26 | renderFunctionParameter(dependencies, importsInBindingsFile, param) 27 | ); 28 | const returnType = renderFunctionReturnType( 29 | dependencies, 30 | importsInBindingsFile, 31 | entry.result, 32 | parameterStrings, 33 | ); 34 | entriesInBindingsFile.push(createDummyRenderDataEntry(renderFunctionExport( 35 | namespace__method, 36 | entry.mangling, 37 | parameterStrings, 38 | returnType, 39 | ))); 40 | }; 41 | 42 | export const renderFunctionExport = ( 43 | exportName: string, 44 | mangling: string, 45 | parameters: string[], 46 | result: string, 47 | ) => 48 | `export const ${exportName} = { 49 | name: "${mangling}", 50 | parameters: [${parameters.join(", ")}], 51 | result: ${result}, 52 | } as const; 53 | `; 54 | 55 | export const renderFunctionParameter = ( 56 | dependencies: Set, 57 | importsInBindingsFile: ImportMap, 58 | param: Parameter, 59 | ) => { 60 | const isPassedByValue = isPassableByValue(param.type); 61 | const result = renderTypeAsFfi( 62 | dependencies, 63 | importsInBindingsFile, 64 | param.type, 65 | ); 66 | if (isPassedByValue) { 67 | return result; 68 | } 69 | let useBuffer = true; 70 | if (isStruct(param.type)) { 71 | // Use buffer if explicitly asked or if not explicitly asked to use pointer. 72 | useBuffer = param.type.usedAsBuffer || !param.type.usedAsPointer; 73 | } else if (isInlineTemplateStruct(param.type)) { 74 | if (!param.type.specialization) { 75 | param.type.specialization = param.type.template.defaultSpecialization!; 76 | } 77 | useBuffer = param.type.specialization.usedAsBuffer || 78 | !param.type.specialization.usedAsPointer; 79 | } 80 | if (useBuffer) { 81 | importsInBindingsFile.set("buf", SYSTEM_TYPES); 82 | return `buf(${result})`; 83 | } else { 84 | importsInBindingsFile.set("ptr", SYSTEM_TYPES); 85 | return `ptr(${result})`; 86 | } 87 | }; 88 | 89 | export const renderFunctionReturnType = ( 90 | dependencies: Set, 91 | importsInBindingsFile: ImportMap, 92 | resultType: null | TypeEntry, 93 | parameterStrings: string[], 94 | ) => { 95 | const result = renderTypeAsFfi( 96 | dependencies, 97 | importsInBindingsFile, 98 | resultType, 99 | ); 100 | if (!isPassableByValue(resultType)) { 101 | importsInBindingsFile.set("buf", SYSTEM_TYPES); 102 | parameterStrings.unshift(`buf(${result})`); 103 | return `"void"`; 104 | } 105 | return result; 106 | }; 107 | -------------------------------------------------------------------------------- /lib/visitors/Function.ts: -------------------------------------------------------------------------------- 1 | import { CXCursor } from "https://deno.land/x/libclang@1.0.0-beta.8/mod.ts"; 2 | import { Context } from "../Context.ts"; 3 | import type { Parameter, TypeEntry } from "../types.d.ts"; 4 | import { 5 | isInlineTemplateStruct, 6 | isPassableByValue, 7 | isPointer, 8 | isStruct, 9 | } from "../utils.ts"; 10 | import { visitType } from "./Type.ts"; 11 | 12 | export const visitFunctionCursor = ( 13 | context: Context, 14 | cursor: CXCursor, 15 | ) => { 16 | const parameters: Parameter[] = []; 17 | const parameterCount = cursor.getNumberOfArguments(); 18 | for (let i = 0; i < parameterCount; i++) { 19 | const arg = cursor.getArgument(i); 20 | if (!arg) { 21 | throw new Error( 22 | `Could not find argument at index ${i} of function '${cursor.getSpelling()}'`, 23 | ); 24 | } 25 | const name = arg.getSpelling(); 26 | const paramType = arg.getType(); 27 | if (!paramType) { 28 | throw new Error( 29 | `Could not get argument type of argument '${name}' of function '${cursor.getSpelling()}'`, 30 | ); 31 | } 32 | let type: TypeEntry | null; 33 | try { 34 | type = visitType(context, paramType); 35 | } catch (err) { 36 | const newError = new Error( 37 | `Failed to visit type of argument '${name}' of function '${cursor.getSpelling()}'`, 38 | ); 39 | newError.cause = err; 40 | throw newError; 41 | } 42 | if (type === null) { 43 | throw new Error( 44 | `Type of argument '${name}' of function '${cursor.getSpelling()}' was void`, 45 | ); 46 | } 47 | if (typeof type === "object" && "used" in type) { 48 | type.used = true; 49 | } 50 | 51 | if (isStruct(type) && !isPassableByValue(type)) { 52 | // Pass-by-value struct as a parameter only accepts Uint8Arrays in Deno. 53 | type.usedAsBuffer = true; 54 | } else if (isInlineTemplateStruct(type) && !isPassableByValue(type)) { 55 | if (!type.specialization) { 56 | type.specialization = type.template.defaultSpecialization!; 57 | } 58 | type.specialization.usedAsBuffer = true; 59 | } 60 | 61 | parameters.push({ 62 | kind: "parameter", 63 | comment: null, 64 | name, 65 | type, 66 | }); 67 | } 68 | const rvType = cursor.getResultType(); 69 | if (!rvType) { 70 | throw new Error( 71 | `Could not get return value type of function '${cursor.getSpelling()}'`, 72 | ); 73 | } 74 | try { 75 | const result = visitType(context, rvType); 76 | if (result !== null && typeof result === "object" && "used" in result) { 77 | result.used = true; 78 | } 79 | 80 | if (isStruct(result)) { 81 | // By-value struct returns as a Uint8Array, 82 | // by-ref struct takes an extra Uint8Array parameter. 83 | // Either way, the struct ends up as a buffer. 84 | result.usedAsBuffer = true; 85 | } else if (isInlineTemplateStruct(result)) { 86 | // Same thing as above: One way or another the template 87 | // instance struct ends up as a buffer. 88 | if (!result.specialization) { 89 | result.specialization = result.template.defaultSpecialization!; 90 | } 91 | result.specialization.usedAsBuffer = true; 92 | } else if (isPointer(result)) { 93 | if (isStruct(result.pointee)) { 94 | result.pointee.usedAsPointer = true; 95 | } else if (isInlineTemplateStruct(result.pointee)) { 96 | if (!result.pointee.specialization) { 97 | result.pointee.specialization = result.pointee.template 98 | .defaultSpecialization!; 99 | } 100 | result.pointee.specialization.usedAsPointer = true; 101 | } 102 | } 103 | 104 | return { 105 | parameters, 106 | result, 107 | }; 108 | } catch (err) { 109 | const newError = new Error( 110 | `Failed to visit return value type of '${cursor.getSpelling()}'`, 111 | ); 112 | newError.cause = err; 113 | throw newError; 114 | } 115 | }; 116 | -------------------------------------------------------------------------------- /lib/renderers/Enum.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CXChildVisitResult, 3 | CXCursor, 4 | CXCursorKind, 5 | CXTypeKind, 6 | } from "https://deno.land/x/libclang@1.0.0-beta.8/mod.ts"; 7 | import { EnumEntry, RenderData } from "../types.d.ts"; 8 | import { createRenderDataEntry } from "../utils.ts"; 9 | import { renderTypeAsFfi, renderTypeAsTS } from "./Type.ts"; 10 | 11 | interface EnumValue { 12 | name: string; 13 | value: null | string | number; 14 | comment: string | null; 15 | } 16 | 17 | export const renderEnum = ({ 18 | entriesInTypesFile, 19 | importsInTypesFile, 20 | }: RenderData, entry: EnumEntry) => { 21 | const dependencies = new Set(); 22 | const EnumT = renderTypeAsFfi(dependencies, importsInTypesFile, entry); 23 | const Enum = renderTypeAsTS(dependencies, importsInTypesFile, entry); 24 | const refT = renderTypeAsFfi(dependencies, importsInTypesFile, entry.type); 25 | entriesInTypesFile.push( 26 | createRenderDataEntry( 27 | [EnumT, Enum], 28 | [...dependencies], 29 | `export const ${EnumT} = ${refT}; 30 | export const enum ${Enum} { 31 | ${ 32 | getEnumValues(entry.cursor).map((value) => 33 | ` ${value.name}${value.value === null ? "" : ` = ${value.value}`},` 34 | ).join("\n ") 35 | } 36 | } 37 | `, 38 | ), 39 | ); 40 | }; 41 | 42 | const getEnumValues = ( 43 | typeDeclaration: CXCursor, 44 | ): EnumValue[] => { 45 | const enumType = typeDeclaration.getEnumDeclarationIntegerType()!; 46 | const canonicalKind = enumType.getCanonicalType().kind; 47 | const isUnsignedInt = canonicalKind === CXTypeKind.CXType_Bool || 48 | canonicalKind === CXTypeKind.CXType_Char_U || 49 | canonicalKind === CXTypeKind.CXType_UChar || 50 | canonicalKind === CXTypeKind.CXType_UShort || 51 | canonicalKind === CXTypeKind.CXType_UInt || 52 | canonicalKind === CXTypeKind.CXType_ULong || 53 | canonicalKind === CXTypeKind.CXType_ULongLong; 54 | const values: EnumValue[] = []; 55 | let previousRawComment = ""; 56 | typeDeclaration.visitChildren((child, parent) => { 57 | if (child.kind === CXCursorKind.CXCursor_EnumConstantDecl) { 58 | const rawComment = child.getRawCommentText(); 59 | let comment: string | null; 60 | if (rawComment === previousRawComment) { 61 | // "Inherited" comment, do not duplicate it 62 | comment = null; 63 | } else { 64 | previousRawComment = rawComment; 65 | comment = null; 66 | } 67 | values.push({ 68 | comment, 69 | name: child.getSpelling(), 70 | value: null, 71 | }); 72 | return CXChildVisitResult.CXChildVisit_Recurse; 73 | } else if (child.kind === CXCursorKind.CXCursor_IntegerLiteral) { 74 | const last = values.at(-1)!; 75 | last.value = Number( 76 | isUnsignedInt 77 | ? parent.getEnumConstantDeclarationUnsignedValue() 78 | : parent.getEnumConstantDeclarationValue(), 79 | ); 80 | } else if (child.kind === CXCursorKind.CXCursor_DeclRefExpr) { 81 | const last = values.at(-1)!; 82 | last.value = child.getSpelling(); 83 | } else { 84 | const last = values.at(-1)!; 85 | const policy = parent.getPrintingPolicy(); 86 | policy.includeNewlines = false; 87 | const prettyPrintedParent = parent.getPrettyPrinted(policy); 88 | policy.dispose(); 89 | const assignmentPrefix = `${last.name} = `; 90 | if (!prettyPrintedParent.startsWith(assignmentPrefix)) { 91 | last.value = Number( 92 | isUnsignedInt 93 | ? parent.getEnumConstantDeclarationUnsignedValue() 94 | : parent.getEnumConstantDeclarationValue(), 95 | ); 96 | } else { 97 | last.value = prettyPrintedParent.substring(assignmentPrefix.length); 98 | } 99 | if (typeof last.value === "string" && last.value.endsWith("U")) { 100 | last.value = last.value.substring(0, last.value.length - 1); 101 | } 102 | if (typeof last.value === "string" && last.value.includes("::")) { 103 | last.value = last.value.replaceAll("::", "."); 104 | } 105 | if ( 106 | !Number.isNaN(Number(last.value)) && 107 | String(Number(last.value)) === last.value 108 | ) { 109 | last.value = Number(last.value); 110 | } 111 | } 112 | return CXChildVisitResult.CXChildVisit_Continue; 113 | }); 114 | let maxHexadecimalLength = 0; 115 | if ( 116 | values.length >= 3 && 117 | values.every((value) => { 118 | if (typeof value.value === "string") { 119 | return true; 120 | } 121 | const isHexadecimalReady = typeof value.value === "number" && 122 | (value.value === 0 || 123 | Number.isInteger(Math.log(value.value) / Math.log(2)) || 124 | value.value > 0x1000); 125 | if (isHexadecimalReady) { 126 | maxHexadecimalLength = value.value!.toString(16).length; 127 | } 128 | return isHexadecimalReady; 129 | }) && 130 | !(values.length === 3 && 131 | values.every((value) => 132 | typeof value.value === "number" && value.value === (value.value & 0b11) 133 | )) 134 | ) { 135 | // Enum of powers of two, use hexadecimal formatting 136 | for (const value of values) { 137 | if (typeof value.value === "string") { 138 | continue; 139 | } 140 | value.value = `0x${ 141 | value.value!.toString(16).padStart(maxHexadecimalLength, "0") 142 | }`; 143 | } 144 | } 145 | return values; 146 | }; 147 | -------------------------------------------------------------------------------- /deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2", 3 | "remote": { 4 | "https://deno.land/std@0.170.0/_util/asserts.ts": "d0844e9b62510f89ce1f9878b046f6a57bf88f208a10304aab50efcb48365272", 5 | "https://deno.land/std@0.170.0/_util/os.ts": "8a33345f74990e627b9dfe2de9b040004b08ea5146c7c9e8fe9a29070d193934", 6 | "https://deno.land/std@0.170.0/path/_constants.ts": "df1db3ffa6dd6d1252cc9617e5d72165cd2483df90e93833e13580687b6083c3", 7 | "https://deno.land/std@0.170.0/path/_interface.ts": "ee3b431a336b80cf445441109d089b70d87d5e248f4f90ff906820889ecf8d09", 8 | "https://deno.land/std@0.170.0/path/_util.ts": "d16be2a16e1204b65f9d0dfc54a9bc472cafe5f4a190b3c8471ec2016ccd1677", 9 | "https://deno.land/std@0.170.0/path/common.ts": "bee563630abd2d97f99d83c96c2fa0cca7cee103e8cb4e7699ec4d5db7bd2633", 10 | "https://deno.land/std@0.170.0/path/glob.ts": "81cc6c72be002cd546c7a22d1f263f82f63f37fe0035d9726aa96fc8f6e4afa1", 11 | "https://deno.land/std@0.170.0/path/mod.ts": "cf7cec7ac11b7048bb66af8ae03513e66595c279c65cfa12bfc07d9599608b78", 12 | "https://deno.land/std@0.170.0/path/posix.ts": "b859684bc4d80edfd4cad0a82371b50c716330bed51143d6dcdbe59e6278b30c", 13 | "https://deno.land/std@0.170.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9", 14 | "https://deno.land/std@0.170.0/path/win32.ts": "7cebd2bda6657371adc00061a1d23fdd87bcdf64b4843bb148b0b24c11b40f69", 15 | "https://deno.land/x/case@2.1.1/camelCase.ts": "0808961e69e1883c6e94faf85e333196a464ceba46ddc76984d7b8ce9e26a43a", 16 | "https://deno.land/x/case@2.1.1/constantCase.ts": "e50eaa8a45cf68417902fda218f03a99e45e2a30f7fce2db325306c84e8c3f0a", 17 | "https://deno.land/x/case@2.1.1/dotCase.ts": "ef3977567057e6c2a4e1b5ca09ec34b9e0788c1a50246187c11f435a468be17e", 18 | "https://deno.land/x/case@2.1.1/headerCase.ts": "4440a251a196fb6d7090213c39e4c6c50ddecab90c14ec91495bee3563082d3c", 19 | "https://deno.land/x/case@2.1.1/lowerCase.ts": "86d5533f9587ed60003181591e40e648838c23f371edfa79d00288153d113b16", 20 | "https://deno.land/x/case@2.1.1/lowerFirstCase.ts": "74e8ebe10f3c54a9d8e81d21127a20fcfb34c446e9c49b2a335162babd652de9", 21 | "https://deno.land/x/case@2.1.1/mod.ts": "28b0b1329c7b18730799ac05627a433d9547c04b9bfb429116247c60edecd97b", 22 | "https://deno.land/x/case@2.1.1/normalCase.ts": "6a8b924da9ab0790d99233ae54bfcfc996d229cb91b2533639fe20972cc33dac", 23 | "https://deno.land/x/case@2.1.1/paramCase.ts": "cf3101c59fd4f16ee14fd09985adb7fa3dbfb194f102499986f7407995202394", 24 | "https://deno.land/x/case@2.1.1/pascalCase.ts": "f68936d584182c8f4b341238bd9c424b1a740bfba3ab2847234f57a4c205f1df", 25 | "https://deno.land/x/case@2.1.1/pathCase.ts": "76e5f437369f8981e17ecdb07870e1c9c8d2d0357f1bf3377e2b0eb132159ed8", 26 | "https://deno.land/x/case@2.1.1/sentenceCase.ts": "f3355985a6a41e088c8c9be80219e5e055a68ad9a987df084a57ee437a0871c5", 27 | "https://deno.land/x/case@2.1.1/snakeCase.ts": "ee2ab4e2c931d30bb79190d090c21eb5c00d1de1b7a9a3e7f33e035ae431333b", 28 | "https://deno.land/x/case@2.1.1/swapCase.ts": "d9b5ee5b8e1ee3d202cbce32a504dde976b9002ed94b4527054a004179905dbb", 29 | "https://deno.land/x/case@2.1.1/titleCase.ts": "36d3fc73df58712240a74b04d84191ac22dd2866bef3838b02157f8e46cb0ecb", 30 | "https://deno.land/x/case@2.1.1/types.ts": "8e2bd6edaa27c0d1972c0d5b76698564740f37b4d3787d58d1fb5f48de611e61", 31 | "https://deno.land/x/case@2.1.1/upperCase.ts": "e6a1a3dea30e17013aa212ec237b35e2dd5c5c0b1501778c07db66ff0bbe4372", 32 | "https://deno.land/x/case@2.1.1/upperFirstCase.ts": "2b083db95744105a4f4accefe66dcd09390634514abf757fc580a7ebad58bf4f", 33 | "https://deno.land/x/case@2.1.1/vendor/camelCaseRegexp.ts": "7d9ff02aad4ab6429eeab7c7353f7bcdd6cc5909a8bd3dda97918c8bbb7621ae", 34 | "https://deno.land/x/case@2.1.1/vendor/camelCaseUpperRegexp.ts": "292de54a698370f90adcdf95727993d09888b7f33d17f72f8e54ba75f7791787", 35 | "https://deno.land/x/case@2.1.1/vendor/nonWordRegexp.ts": "c1a052629a694144b48c66b0175a22a83f4d61cb40f4e45293fc5d6b123f927e", 36 | "https://deno.land/x/libclang@1.0.0-beta.8/BuildSystem.ts": "02d72dd221396972e16cdbeaf6117a33ec5a4bfe1a134cb94c5a8aaaa5f7713e", 37 | "https://deno.land/x/libclang@1.0.0-beta.8/CXCompilationDatabase.ts": "16a82acf5de22d0220a9abeda2b23f78d7962b11975a00561a894ab56aafa127", 38 | "https://deno.land/x/libclang@1.0.0-beta.8/ffi.ts": "d3ca6faf774a2511afea3c686440fa6bf87cd0caa6bfc6a488854f539ad74a4d", 39 | "https://deno.land/x/libclang@1.0.0-beta.8/include/BuildSystem.h.ts": "ba26f2a01b55723e5eb985e3156e9b7d6894954a352bbcebad6992fe99f41ee5", 40 | "https://deno.land/x/libclang@1.0.0-beta.8/include/CXCompilationDatabase.h.ts": "6df6c832a0d4e0469f8c8b0aeecc9017097d0cd69a66383ae15ed0fff45e39f9", 41 | "https://deno.land/x/libclang@1.0.0-beta.8/include/CXDiagnostic.h.ts": "08cd0444ad9806f03a779731c4a026d9aaea0e22bb2a336a1b546f656619e426", 42 | "https://deno.land/x/libclang@1.0.0-beta.8/include/CXFile.h.ts": "b86b4aa735bf920ada5e79775ce5a31cd7786007fa5957713d92c7f18a5eec60", 43 | "https://deno.land/x/libclang@1.0.0-beta.8/include/CXSourceLocation.h.ts": "6e87d111e0ca8fee4050362723372459ebc01694ca4fed6aeb08fe8cc401fe9b", 44 | "https://deno.land/x/libclang@1.0.0-beta.8/include/CXString.h.ts": "9b2b83f418eff5565668200ffc2a0cbbd8d7090d0bbe1eb7b229633788aa7a3a", 45 | "https://deno.land/x/libclang@1.0.0-beta.8/include/Documentation.h.ts": "9fe0b0071ee01438cd5d52447e8a0d96bb8f8700b8b68bada3c0db3f001e9382", 46 | "https://deno.land/x/libclang@1.0.0-beta.8/include/FatalErrorHandler.h.ts": "094dc535116bc2d1eb83fdc52c00c93fceea5a1c011fbd2446f921bcb611cf34", 47 | "https://deno.land/x/libclang@1.0.0-beta.8/include/Index.h.ts": "19317fac65e39a7b45f249d09c524e573b8de4fba058cb4fdee766e81acf435d", 48 | "https://deno.land/x/libclang@1.0.0-beta.8/include/Rewrite.h.ts": "f38028a4cef48200d26d9b5b16412919972e6e0b19518659b6a00cf8c9d0a2fe", 49 | "https://deno.land/x/libclang@1.0.0-beta.8/include/typeDefinitions.ts": "898122d7687c7237bbe53d7b47a941d1393cadbb63212fb4cb048d5dfaa8e9d8", 50 | "https://deno.land/x/libclang@1.0.0-beta.8/mod.ts": "937ca20a475ba6fc24c25f793176233dcefc64851cc72326e33055e63ebc8ae3", 51 | "https://deno.land/x/libclang@1.0.0-beta.8/utils.ts": "797d1686287966e61db8b83631ee005b0e9d88ac3906fcc56beda139ce5b56e8" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # txx 2 | 3 | Unsafe interop between Deno FFI and C++ 4 | 5 | The library is currently only tested and supported on Linux. 6 | 7 | ## Usage 8 | 9 | The only entry point to the library is the `build` function exported from the 10 | `mod.ts` file. It is called with an `ExportConfiguration` object which 11 | determines from where, what and where to the txx build should generate bindings. 12 | 13 | The txx build generates up to three TypeScript files per header file: 14 | 15 | 1. Types file exports struct, enum, and typedef definitions in Deno FFI format. 16 | The types file is always named `header.h.types.ts`. 17 | 2. Exports file exports only the Deno FFI function and variable bindings. The 18 | exports file is always named `header.h.ts`. 19 | 3. Classes file exports TypeScript class definitions of C++ classes. The class 20 | file is always named `header.h.classes.ts`. 21 | 22 | The TypeScript files for each header are generated in the equivalent directory 23 | location of the source header, relative to the `basePath` configuration. The 24 | contents of the header files are determined by the `imports` configuration and 25 | what those configured imports require for full functionality. Additionally the 26 | build generates an `ffi.ts` file that exports all the loaded FFI bindings. The 27 | library import path for the `Deno.dlopen()` call is a static string that has no 28 | chance of working and must be changed manually after the build has completed. 29 | 30 | After the build, the files should fully type-check with 0 errors. The bindings 31 | should be usable as is except for the aforementioned library import path and the 32 | TypeScript class definitions exported from classes files should provide a 33 | type-safe entry point to the raw FFI bindings for further development. Note that 34 | it is not safe to expose the raw classes to users: The classes extend 35 | `Uint8Array` and thus they always carry a "mutable reference" to the class data 36 | with them. Their APIs are always perhaps type-safe but they are definitely not 37 | safe from bad JavaScript or `--no-check` usage: Allowing users free reign to 38 | these classes will lead to segmentation faults and memory corruption. It is 39 | always best to write a safe wrapping layer over the raw classes; preferably keep 40 | the raw classes entirely hidden by way of JavaScript private class properties 41 | and/or internal Symbol slots. 42 | 43 | See `examples/basic_usage.ts` for a simple example of usage. 44 | 45 | ## `ExportConfiguration` 46 | 47 | ### `basePath` 48 | 49 | This base path determines the path that the build considers to be the "root" of 50 | all header files. When header files get translated to TypeScript files, the 51 | output files are written into an equivalent directory path as the original 52 | header file was in relative to the base path. 53 | 54 | If a header file gets included from outside the base path, then its contents get 55 | rolled into special "system" output files at the root of the output path. 56 | Generally these system output files only include helper functions and C++ `std` 57 | library definitions. If you find that your build generates large amounts of 58 | definitions in the system files, your base path is likely too "low" in your 59 | header directory. 60 | 61 | ### `outputPath` 62 | 63 | This output path determines the path into which output files are written. 64 | 65 | Notable output files are `${outputPath}/ffi.ts` which includes the 66 | `Deno.dlopen()` call and exports all of the bindings from it, as well as 67 | `${outputPath}/systemTypes.ts` which includes helper functions and (usually) C++ 68 | `std` library definitions. 69 | 70 | ### `imports` 71 | 72 | This array defines which entries from the headers should be generated into the 73 | bindings. 74 | 75 | The currently supported imports are classes and functions. Variables (statics / 76 | constants) are also mentioned in the types but are not yet supported by the 77 | library. 78 | 79 | Functions are fairly simple: Binding a function means that all of its parameter 80 | types and its result type get generated in the output. The function is bound and 81 | exported from the `${outputPath}/ffi.ts` file. 82 | 83 | Classes are the most interesting thing here: Using the configuration you can 84 | choose if and how constructors, destructors and methods are wanted in the 85 | output. All function-like entries defined in this way include their parameter 86 | types and result type in the generated output. Additionally, the class itself is 87 | generated as a TypeScript class extending `Uint8Array` (or a base class if the 88 | class inherits another class). These class definitions will include any wanted 89 | constructors as static methods that return an instance of the class. As a result 90 | your raw class usage will look something like this: 91 | 92 | ```ts 93 | const u32 = 12; 94 | const classEntry = MyClass.Constructor(u32); // Calls the C++ constructor `lib::MyClass::Constructor(uint32_t)` 95 | 96 | classEntry.print(); // Calls the C++ method `lib::MyClass::print()` 97 | 98 | classEntry.delete(); // Calls the C++ destructor `lib::MyClass::~MyClass()` 99 | ``` 100 | 101 | ### `files` 102 | 103 | This array of files determines which headers are initially included into the 104 | build. The list gets turned into a temporary header file of include commands, 105 | which is then given to Clang as the entry point. 106 | 107 | ### `include` 108 | 109 | This array of directories is the list of include paths given to Clang. It 110 | determines where Clang will look for any included headers and in what order. 111 | 112 | This list should include the same folders that the normal build of this dynamic 113 | library done with as well as your Clang's C++ standard library headers 114 | directory. 115 | 116 | ## Features 117 | 118 | - Functions 119 | - Classes 120 | - Class inheritance 121 | - Structs 122 | - Enums 123 | - Typedefs 124 | - Basic class templates 125 | - Partial class specializations (only partially done) 126 | 127 | ## Not yet supported 128 | 129 | - Statics / constants (variables) 130 | - Full heuristics for buffer vs pointer types for class references 131 | - Deno-side inheritance of C++ classes 132 | - Template aliases 133 | - Namespaces with overlapping definitions: All namespaces are collapsed into 134 | one. 135 | - Multiple inheritance 136 | - Windows, Mac OS X 137 | -------------------------------------------------------------------------------- /lib/renderer.ts: -------------------------------------------------------------------------------- 1 | import { renderClass } from "./renderers/Class.ts"; 2 | import { renderClassTemplate } from "./renderers/ClassTemplate.ts"; 3 | import { renderEnum } from "./renderers/Enum.ts"; 4 | import { renderFunction } from "./renderers/Function.ts"; 5 | import { renderTypedef } from "./renderers/Typedef.ts"; 6 | import { renderUnion } from "./renderers/Union.ts"; 7 | import { renderVar } from "./renderers/Var.ts"; 8 | import { 9 | AbsoluteFilePath, 10 | ImportMap, 11 | RenderData, 12 | RenderDataEntry, 13 | UseableEntry, 14 | } from "./types.d.ts"; 15 | import { 16 | bindingsFile, 17 | classesFile, 18 | FFI, 19 | sortRenderDataEntries, 20 | SYSTEM_BINDINGS, 21 | SYSTEM_CLASSES, 22 | SYSTEM_TYPES, 23 | typesFile, 24 | } from "./utils.ts"; 25 | 26 | export const renderFile = ( 27 | filePath: AbsoluteFilePath, 28 | data: UseableEntry[], 29 | ) => { 30 | const bindingsFilePath = bindingsFile(filePath); 31 | const classesFilePath = classesFile(filePath); 32 | const typesFilePath = typesFile(filePath); 33 | const importsInBindingsFile: ImportMap = new Map(); 34 | const importsInClassesFile: ImportMap = new Map(); 35 | const importsInTypesFile: ImportMap = new Map(); 36 | const entriesInBindingsFile: RenderDataEntry[] = []; 37 | const entriesInClassesFile: RenderDataEntry[] = []; 38 | const entriesInTypesFile: RenderDataEntry[] = []; 39 | const memory: RenderData = { 40 | bindings: new Set(), 41 | bindingsFilePath, 42 | classesFilePath, 43 | typesFilePath, 44 | importsInBindingsFile, 45 | importsInClassesFile, 46 | importsInTypesFile, 47 | entriesInBindingsFile, 48 | entriesInClassesFile, 49 | entriesInTypesFile, 50 | }; 51 | 52 | for (const entry of data) { 53 | switch (entry.kind) { 54 | case "enum": 55 | renderEnum(memory, entry); 56 | break; 57 | case "function": 58 | renderFunction(memory, entry); 59 | break; 60 | case "class": 61 | renderClass(memory, entry); 62 | break; 63 | case "class": 64 | renderClassTemplate(memory, entry); 65 | break; 66 | case "typedef": 67 | renderTypedef(memory, entry); 68 | break; 69 | case "union": 70 | renderUnion(memory, entry); 71 | break; 72 | case "var": 73 | renderVar(memory, entry); 74 | break; 75 | } 76 | } 77 | 78 | sortRenderDataEntries(entriesInClassesFile); 79 | sortRenderDataEntries(entriesInTypesFile); 80 | 81 | return memory; 82 | }; 83 | 84 | export const handleImports = ( 85 | basePath: AbsoluteFilePath, 86 | memory: Set, 87 | importMap: ImportMap, 88 | ) => { 89 | for (const [importString, importPath] of importMap) { 90 | if ( 91 | importString.startsWith("type ") && 92 | importMap.has(importString.substring(5)) 93 | ) { 94 | // Do not import `type Foo` if `Foo` is also needed. 95 | importMap.delete(importString); 96 | continue; 97 | } 98 | if (!importPath.startsWith("#") && !importPath.endsWith(".ts")) { 99 | throw new Error( 100 | `Invalid import path: 'import { ${importString} } from "${importPath}";'`, 101 | ); 102 | } 103 | if (importPath === SYSTEM_TYPES) { 104 | // Gather up imports used from "system" 105 | memory.add(importString); 106 | } else if (importPath !== FFI && !importPath.startsWith(basePath)) { 107 | // Import from outside the base path: These are redirected 108 | // to the #SYSTEM files 109 | if (importPath.endsWith(".classes.ts")) { 110 | importMap.set(importString, SYSTEM_CLASSES); 111 | } else if (importPath.endsWith(".types.ts")) { 112 | importMap.set(importString, SYSTEM_TYPES); 113 | } else { 114 | importMap.set(importString, SYSTEM_BINDINGS); 115 | } 116 | } 117 | } 118 | }; 119 | 120 | export const renderSystemFileConstant = (constant: string): RenderDataEntry => { 121 | let contents: string; 122 | let names = [constant]; 123 | switch (constant) { 124 | case "buf": 125 | case "type Buf": 126 | names = ["buf", "type Buf"]; 127 | contents = `declare const BufBrand: unique symbol; 128 | export type Buf = "buffer" & { [BufBrand]: T }; 129 | export const buf = (_: T) => "buffer" as Buf; 130 | `; 131 | break; 132 | case "ptr": 133 | case "type Ptr": 134 | names = ["ptr", "type Ptr"]; 135 | contents = `declare const PtrBrand: unique symbol; 136 | export type Ptr = "pointer" & { [PtrBrand]: T }; 137 | export const ptr = (_: T) => "pointer" as Ptr; 138 | `; 139 | break; 140 | case "func": 141 | case "type Func": 142 | names = ["func", "type Func"]; 143 | contents = `declare const FuncBrand: unique symbol; 144 | export type Func = "function" & { [FuncBrand]: T }; 145 | export const func = (_: T) => "function" as Func; 146 | `; 147 | break; 148 | case "union2": 149 | contents = 150 | `export const union2 = (a: T, _b: U): T | U => a; 151 | `; 152 | break; 153 | case "union3": 154 | contents = 155 | `export const union3 = (a: T, _b: U, _c: V): T | U | V => a; 156 | `; 157 | break; 158 | case "union4": 159 | contents = 160 | `export const union4 = (a: T, _b: U, _c: V, _d: W): T | U | V | W => a; 161 | `; 162 | break; 163 | case "union5": 164 | contents = 165 | `export const union5 = (a: T, _b: U, _c: V, _d: W, _e: X): T | U | V | W | X => a; 166 | `; 167 | break; 168 | case "type cstringT": 169 | case "cstringT": 170 | contents = `export const cstringT = "buffer"; 171 | `; 172 | break; 173 | case "type cstringArrayT": 174 | case "cstringArrayT": 175 | contents = `export const cstringT = "buffer"; 176 | `; 177 | break; 178 | case "isFunction": 179 | contents = 180 | `export const isFunction = (type: unknown): type is Deno.UnsafeCallbackDefinition => type !== null && typeof type === "object" && "parameters" in type && Array.isArray(type.parameters) && "result" in type; 181 | `; 182 | break; 183 | default: 184 | if (constant.startsWith("union")) { 185 | contents = 186 | `export const ${constant} = (...args: unknown[]) => args[0] as { struct: string[] };`; 187 | } else { 188 | throw new Error(`Unsupported file system constant '${constant}`); 189 | } 190 | } 191 | return { 192 | contents, 193 | dependencies: [], 194 | names, 195 | }; 196 | }; 197 | -------------------------------------------------------------------------------- /tests/output/std_function.h.classes.ts: -------------------------------------------------------------------------------- 1 | import { MyClass__Constructor, PodClass__create } from "./ffi.ts"; 2 | import { 3 | BINARY_CALLBACK_SIZE, 4 | MY_CLASS_SIZE, 5 | NON_POD_CLASS_SIZE, 6 | NULLARY_CALLBACK_SIZE, 7 | OTHER_POD_CLASS_SIZE, 8 | POD_CLASS_SIZE, 9 | type PodClassPointer, 10 | TERNARY_CALLBACK_SIZE, 11 | UNARY_CALLBACK_SIZE, 12 | } from "./std_function.h.types.ts"; 13 | import { functionBuffer } from "./systemClasses.ts"; 14 | import { type Buf } from "./systemTypes.ts"; 15 | 16 | export class NullaryCallbackBuffer 17 | extends functionBuffer> { 18 | constructor(arg?: ArrayBufferLike | number) { 19 | if (typeof arg === "undefined") { 20 | super(NULLARY_CALLBACK_SIZE); 21 | return; 22 | } else if (typeof arg === "number") { 23 | if (!Number.isFinite(arg) || arg < NULLARY_CALLBACK_SIZE) { 24 | throw new Error( 25 | "Invalid construction of NullaryCallbackBuffer: Size is not finite or is too small", 26 | ); 27 | } 28 | super(arg); 29 | return; 30 | } 31 | if (arg.byteLength < NULLARY_CALLBACK_SIZE) { 32 | throw new Error( 33 | "Invalid construction of NullaryCallbackBuffer: Buffer size is too small", 34 | ); 35 | } 36 | super(arg); 37 | } 38 | } 39 | 40 | export class UnaryCallbackBuffer 41 | extends functionBuffer> { 42 | constructor(arg?: ArrayBufferLike | number) { 43 | if (typeof arg === "undefined") { 44 | super(UNARY_CALLBACK_SIZE); 45 | return; 46 | } else if (typeof arg === "number") { 47 | if (!Number.isFinite(arg) || arg < UNARY_CALLBACK_SIZE) { 48 | throw new Error( 49 | "Invalid construction of UnaryCallbackBuffer: Size is not finite or is too small", 50 | ); 51 | } 52 | super(arg); 53 | return; 54 | } 55 | if (arg.byteLength < UNARY_CALLBACK_SIZE) { 56 | throw new Error( 57 | "Invalid construction of UnaryCallbackBuffer: Buffer size is too small", 58 | ); 59 | } 60 | super(arg); 61 | } 62 | } 63 | 64 | export class BinaryCallbackBuffer extends functionBuffer< 65 | Deno.UnsafeCallbackDefinition<["i32", "i32"], "void"> 66 | > { 67 | constructor(arg?: ArrayBufferLike | number) { 68 | if (typeof arg === "undefined") { 69 | super(BINARY_CALLBACK_SIZE); 70 | return; 71 | } else if (typeof arg === "number") { 72 | if (!Number.isFinite(arg) || arg < BINARY_CALLBACK_SIZE) { 73 | throw new Error( 74 | "Invalid construction of BinaryCallbackBuffer: Size is not finite or is too small", 75 | ); 76 | } 77 | super(arg); 78 | return; 79 | } 80 | if (arg.byteLength < BINARY_CALLBACK_SIZE) { 81 | throw new Error( 82 | "Invalid construction of BinaryCallbackBuffer: Buffer size is too small", 83 | ); 84 | } 85 | super(arg); 86 | } 87 | } 88 | 89 | export class TernaryCallbackBuffer extends functionBuffer< 90 | Deno.UnsafeCallbackDefinition<["i32", "i32", Buf<"self">], "void"> 91 | > { 92 | constructor(arg?: ArrayBufferLike | number) { 93 | if (typeof arg === "undefined") { 94 | super(TERNARY_CALLBACK_SIZE); 95 | return; 96 | } else if (typeof arg === "number") { 97 | if (!Number.isFinite(arg) || arg < TERNARY_CALLBACK_SIZE) { 98 | throw new Error( 99 | "Invalid construction of TernaryCallbackBuffer: Size is not finite or is too small", 100 | ); 101 | } 102 | super(arg); 103 | return; 104 | } 105 | if (arg.byteLength < TERNARY_CALLBACK_SIZE) { 106 | throw new Error( 107 | "Invalid construction of TernaryCallbackBuffer: Buffer size is too small", 108 | ); 109 | } 110 | super(arg); 111 | } 112 | } 113 | 114 | export class MyClassBuffer extends Uint8Array { 115 | constructor(arg?: ArrayBufferLike | number) { 116 | if (typeof arg === "undefined") { 117 | super(MY_CLASS_SIZE); 118 | return; 119 | } else if (typeof arg === "number") { 120 | if (!Number.isFinite(arg) || arg < MY_CLASS_SIZE) { 121 | throw new Error( 122 | "Invalid construction of MyClassBuffer: Size is not finite or is too small", 123 | ); 124 | } 125 | super(arg); 126 | return; 127 | } 128 | if (arg.byteLength < MY_CLASS_SIZE) { 129 | throw new Error( 130 | "Invalid construction of MyClassBuffer: Buffer size is too small", 131 | ); 132 | } 133 | super(arg); 134 | } 135 | 136 | static Constructor(self = new MyClassBuffer()): MyClassBuffer { 137 | MyClass__Constructor(self); 138 | return self; 139 | } 140 | } 141 | 142 | export class PodClassBuffer extends Uint8Array { 143 | constructor(arg?: ArrayBufferLike | number) { 144 | if (typeof arg === "undefined") { 145 | super(POD_CLASS_SIZE); 146 | return; 147 | } else if (typeof arg === "number") { 148 | if (!Number.isFinite(arg) || arg < POD_CLASS_SIZE) { 149 | throw new Error( 150 | "Invalid construction of PodClassBuffer: Size is not finite or is too small", 151 | ); 152 | } 153 | super(arg); 154 | return; 155 | } 156 | if (arg.byteLength < POD_CLASS_SIZE) { 157 | throw new Error( 158 | "Invalid construction of PodClassBuffer: Buffer size is too small", 159 | ); 160 | } 161 | super(arg); 162 | } 163 | 164 | static create(): null | PodClassPointer { 165 | return PodClass__create() as null | PodClassPointer; 166 | } 167 | } 168 | 169 | export class OtherPodClassBuffer extends Uint8Array { 170 | constructor(arg?: ArrayBufferLike | number) { 171 | if (typeof arg === "undefined") { 172 | super(OTHER_POD_CLASS_SIZE); 173 | return; 174 | } else if (typeof arg === "number") { 175 | if (!Number.isFinite(arg) || arg < OTHER_POD_CLASS_SIZE) { 176 | throw new Error( 177 | "Invalid construction of OtherPodClassBuffer: Size is not finite or is too small", 178 | ); 179 | } 180 | super(arg); 181 | return; 182 | } 183 | if (arg.byteLength < OTHER_POD_CLASS_SIZE) { 184 | throw new Error( 185 | "Invalid construction of OtherPodClassBuffer: Buffer size is too small", 186 | ); 187 | } 188 | super(arg); 189 | } 190 | } 191 | 192 | export class NonPodClassBuffer extends Uint8Array { 193 | constructor(arg?: ArrayBufferLike | number) { 194 | if (typeof arg === "undefined") { 195 | super(NON_POD_CLASS_SIZE); 196 | return; 197 | } else if (typeof arg === "number") { 198 | if (!Number.isFinite(arg) || arg < NON_POD_CLASS_SIZE) { 199 | throw new Error( 200 | "Invalid construction of NonPodClassBuffer: Size is not finite or is too small", 201 | ); 202 | } 203 | super(arg); 204 | return; 205 | } 206 | if (arg.byteLength < NON_POD_CLASS_SIZE) { 207 | throw new Error( 208 | "Invalid construction of NonPodClassBuffer: Buffer size is too small", 209 | ); 210 | } 211 | super(arg); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /lib/renderers/Type/asTS.ts: -------------------------------------------------------------------------------- 1 | import { ImportMap, TypedefEntry, TypeEntry } from "../../types.d.ts"; 2 | import { 3 | classesFile, 4 | isCharVector, 5 | isFunction, 6 | isInlineTemplateStruct, 7 | isPassableByValue, 8 | isStruct, 9 | isStructLike, 10 | typesFile, 11 | } from "../../utils.ts"; 12 | import { renderTypeAsFfiBindingTypes } from "./asFfiBindingTypes.ts"; 13 | 14 | export const renderTypeAsTS = ( 15 | dependencies: Set, 16 | importMap: ImportMap, 17 | type: null | TypeEntry, 18 | { 19 | intoJS = false, 20 | }: { 21 | intoJS?: boolean; 22 | } = {}, 23 | ): string => { 24 | if (type === null) { 25 | return "void"; 26 | } else if (typeof type === "string") { 27 | switch (type) { 28 | case "bool": 29 | return "boolean"; 30 | case "f32": 31 | case "f64": 32 | case "u8": 33 | case "i8": 34 | case "u16": 35 | case "i16": 36 | case "u32": 37 | case "i32": 38 | return "number"; 39 | case "u64": 40 | case "i64": 41 | return "number | bigint"; 42 | case "pointer": 43 | return "Deno.PointerValue"; 44 | case "buffer": 45 | case "cstring": 46 | case "cstringArray": 47 | if (intoJS) { 48 | return "Deno.PointerValue"; 49 | } 50 | return "Uint8Array"; 51 | default: 52 | throw new Error("Missing match arm"); 53 | } 54 | } 55 | if (type.kind === "enum") { 56 | const name = type.name; 57 | importMap.set(`type ${name}`, typesFile(type.file)); 58 | dependencies.add(name); 59 | return name; 60 | } else if (type.kind === "typedef") { 61 | const name = type.name; 62 | if (isStructLike(type.target)) { 63 | const nameBuffer = `${name}Buffer`; 64 | importMap.set(nameBuffer, classesFile(type.file)); 65 | dependencies.add(nameBuffer); 66 | return nameBuffer; 67 | } else if (intoJS && isCharVector(type.target)) { 68 | // Special handling for cstring and cstringArray and type aliases of those. 69 | return "Deno.PointerValue"; 70 | } 71 | importMap.set(`type ${name}`, typesFile((type as TypedefEntry).file)); 72 | dependencies.add(name); 73 | return name; 74 | } else if ( 75 | type.kind === "function" || type.kind === "fn" 76 | ) { 77 | if ("name" in type && type.name) { 78 | importMap.set(type.name, typesFile(type.file)); 79 | dependencies.add(type.name); 80 | return type.name; 81 | } 82 | return "Deno.PointerValue"; 83 | } else if (type.kind === "pointer") { 84 | if (type.pointee === "self") { 85 | // Self can only appear in callback definitions of classes. 86 | return "Deno.PointerValue"; 87 | } 88 | if (isStruct(type.pointee)) { 89 | if (intoJS || !type.pointee.usedAsBuffer && type.pointee.usedAsPointer) { 90 | // Pointer to struct coming to JS: This is always a pointer object. 91 | // Otherwise if the pointer to a struct is leaving JS and the struct 92 | // is only ever seen as a pointer, use a pointer object. 93 | const namePointer = `${type.pointee.name}Pointer`; 94 | importMap.set(`type ${namePointer}`, typesFile(type.pointee.file)); 95 | dependencies.add(namePointer); 96 | return namePointer; 97 | } else { 98 | // Otherwise us a buffer. 99 | const nameBuffer = `${type.pointee.name}Buffer`; 100 | importMap.set( 101 | `type ${nameBuffer}`, 102 | classesFile(type.pointee.file), 103 | ); 104 | dependencies.add(nameBuffer); 105 | return nameBuffer; 106 | } 107 | } else if (isInlineTemplateStruct(type.pointee)) { 108 | if (!type.pointee.specialization) { 109 | type.pointee.specialization = type.pointee.template 110 | .defaultSpecialization!; 111 | } 112 | if ( 113 | intoJS || 114 | !type.pointee.specialization.usedAsBuffer && 115 | type.pointee.specialization.usedAsPointer 116 | ) { 117 | // Pointer to struct coming to JS: This is always a pointer object. 118 | // Otherwise if the pointer to a struct is leaving JS and the struct 119 | // is only ever seen as a pointer, use a pointer object. 120 | const namePointer = `${type.pointee.name}Pointer`; 121 | importMap.set(`type ${namePointer}`, typesFile(type.pointee.file)); 122 | dependencies.add(namePointer); 123 | return `${namePointer}<${ 124 | type.pointee.parameters.map((param) => 125 | param.kind === "" 126 | ? renderTypeAsFfiBindingTypes(dependencies, importMap, param) 127 | : renderTypeAsFfiBindingTypes(dependencies, importMap, param.type) 128 | ) 129 | }>`; 130 | } else { 131 | // Otherwise us a buffer. 132 | const nameBuffer = `${type.pointee.name}Buffer`; 133 | importMap.set( 134 | `type ${nameBuffer}`, 135 | classesFile(type.pointee.file), 136 | ); 137 | dependencies.add(nameBuffer); 138 | return `${nameBuffer}<${ 139 | type.pointee.parameters.map((param) => 140 | param.kind === "" 141 | ? renderTypeAsFfiBindingTypes(dependencies, importMap, param) 142 | : renderTypeAsFfiBindingTypes(dependencies, importMap, param.type) 143 | ) 144 | }>`; 145 | } 146 | } 147 | if (isStructLike(type.pointee) && type.pointee.name) { 148 | if (intoJS) { 149 | const namePointer = `${type.pointee.name}Pointer`; 150 | importMap.set(`type ${namePointer}`, typesFile(type.pointee.file)); 151 | dependencies.add(namePointer); 152 | return namePointer; 153 | } else { 154 | const nameBuffer = `${type.pointee.name}Buffer`; 155 | importMap.set( 156 | `type ${nameBuffer}`, 157 | classesFile(type.pointee.file), 158 | ); 159 | dependencies.add(nameBuffer); 160 | return nameBuffer; 161 | } 162 | } else if (isFunction(type.pointee)) { 163 | return renderTypeAsTS(dependencies, importMap, type.pointee); 164 | } 165 | return "Deno.PointerValue"; 166 | } else if (type.kind === "class") { 167 | // If class is only seen used as a pointer and we're not being returned 168 | // into JS, then always expect it as a pointer. 169 | const usePointer = !intoJS && isPassableByValue(type) && 170 | !type.usedAsBuffer && type.usedAsPointer; 171 | const name = usePointer ? `${type.name}Pointer` : `${type.name}Buffer`; 172 | importMap.set( 173 | usePointer ? `type ${name}` : name, 174 | classesFile(type.file), 175 | ); 176 | dependencies.add(name); 177 | return name; 178 | } else if (type.kind === "inline class") { 179 | const nameBuffer = `${ 180 | type.specialization?.name || type.template.name 181 | }Buffer`; 182 | importMap.set(nameBuffer, classesFile(type.file)); 183 | dependencies.add(nameBuffer); 184 | return `${nameBuffer}<${ 185 | type.parameters.map((param) => 186 | param.kind === "" 187 | ? renderTypeAsFfiBindingTypes(dependencies, importMap, param) 188 | : renderTypeAsFfiBindingTypes(dependencies, importMap, param.type) 189 | ) 190 | }>`; 191 | } else if ( 192 | type.kind === "inline class" || 193 | type.kind === "[N]" || type.kind === "inline union" 194 | ) { 195 | return "Uint8Array"; 196 | } else if (type.kind === "member pointer") { 197 | return "number"; 198 | } else if (type.kind === "class") { 199 | throw new Error("Unexpected class template entry"); 200 | } else if (type.kind === "union") { 201 | throw new Error("Unexpected union entry"); 202 | } else if (type.kind === "") { 203 | throw new Error("Unexpected template parameter entry"); 204 | } else { 205 | throw new Error( 206 | // @ts-expect-error No type kind should exist here 207 | `internal error: unknown type kind ${type.kind} ${type.name}`, 208 | ); 209 | } 210 | }; 211 | -------------------------------------------------------------------------------- /lib/renderers/ClassTemplate.ts: -------------------------------------------------------------------------------- 1 | import camelCase from "https://deno.land/x/case@2.1.1/camelCase.ts"; 2 | import pascalCase from "https://deno.land/x/case@2.1.1/pascalCase.ts"; 3 | import type { 4 | ClassTemplateEntry, 5 | ClassTemplatePartialSpecialization, 6 | ImportMap, 7 | RenderData, 8 | TemplateParameter, 9 | TypeEntry, 10 | } from "../types.d.ts"; 11 | import { 12 | createDummyRenderDataEntry, 13 | createRenderDataEntry, 14 | isPointer, 15 | } from "../utils.ts"; 16 | import { renderClassBaseField, renderClassField } from "./Class.ts"; 17 | import { renderTypeAsFfi } from "./Type.ts"; 18 | 19 | export const renderClassTemplate = ( 20 | renderData: RenderData, 21 | entry: ClassTemplateEntry, 22 | ) => { 23 | for (const specialization of entry.partialSpecializations) { 24 | if (!specialization.used) { 25 | continue; 26 | } 27 | renderSpecialization( 28 | renderData, 29 | specialization, 30 | entry, 31 | ); 32 | } 33 | if (entry.defaultSpecialization?.used) { 34 | renderSpecialization( 35 | renderData, 36 | entry.defaultSpecialization, 37 | entry, 38 | ); 39 | } 40 | }; 41 | 42 | const renderSpecialization = ( 43 | { entriesInTypesFile, entriesInClassesFile, importsInTypesFile }: RenderData, 44 | specialization: ClassTemplatePartialSpecialization, 45 | entry: ClassTemplateEntry, 46 | ) => { 47 | const ClassT = `${specialization.name}T`; 48 | if (!specialization.cursor.isDefinition()) { 49 | throw new Error( 50 | "Failed to build template class: No specialization matched", 51 | ); 52 | } else if ( 53 | specialization.bases.length === 0 && 54 | specialization.virtualBases.length === 0 && 55 | specialization.fields.length === 0 56 | ) { 57 | return; 58 | } 59 | const dependencies = new Set(); 60 | const replaceMap = new Map(); 61 | 62 | specialization.parameters.forEach((value, index) => { 63 | replaceMap.set(value.name, camelCase(value.name)); 64 | replaceMap.set(`type-parameter-0-${index}`, camelCase(value.name)); 65 | }); 66 | 67 | const inheritedPointers: string[] = []; 68 | const fields: string[] = []; 69 | const fieldRenderOptions = { 70 | dependencies, 71 | inheritedPointers, 72 | importsInTypesFile, 73 | replaceMap, 74 | }; 75 | 76 | for (const base of specialization.bases) { 77 | renderClassBaseField(fieldRenderOptions, fields, base); 78 | } 79 | 80 | for (const field of specialization.fields) { 81 | renderClassField(fieldRenderOptions, fields, field); 82 | } 83 | 84 | for (const base of specialization.virtualBases) { 85 | renderClassBaseField(fieldRenderOptions, fields, base); 86 | } 87 | 88 | if (fields.length === 0) { 89 | return; 90 | } 91 | 92 | if (inheritedPointers.length === 0) { 93 | inheritedPointers.push(`NonNullable`); 94 | } 95 | 96 | const parameterObjects = generateApplicationParameters( 97 | entry.parameters, 98 | specialization.application, 99 | ); 100 | const symbolStrings: string[] = []; 101 | parameterObjects.forEach((param) => { 102 | symbolStrings.push( 103 | `declare const ${specialization.name}__${param.TypeName}: unique symbol;`, 104 | ); 105 | }); 106 | inheritedPointers.push( 107 | `{ [${specialization.name}Template]: unknown; ${ 108 | parameterObjects.map((param) => 109 | `[${specialization.name}__${param.TypeName}]: ${param.TypeName};` 110 | ).join( 111 | " ", 112 | ) 113 | } }`, 114 | ); 115 | 116 | const bodyContents = `{ 117 | ${ 118 | generateDestructuring( 119 | parameterObjects, 120 | dependencies, 121 | importsInTypesFile, 122 | replaceMap, 123 | ) 124 | } 125 | return { struct: [ 126 | ${fields.join("\n ")} 127 | ] } as const; 128 | }`; 129 | 130 | const contents = `declare const ${specialization.name}Template: unique symbol; 131 | ${symbolStrings.join("\n")} 132 | export type ${specialization.name}Pointer<${ 133 | parameterObjects.map(renderPointerTemplateParameter).join(", ") 134 | }> = ${inheritedPointers.join(" & ")}; 135 | export const ${ClassT} = <${ 136 | parameterObjects.map(renderStructTemplateParameter).join(", ") 137 | }>( 138 | ${parameterObjects.map(renderCallTemplateParameter).join("\n ")} 139 | ) => ${bodyContents}; 140 | `; 141 | entriesInTypesFile.push( 142 | createRenderDataEntry([ClassT], [...dependencies], contents), 143 | ); 144 | entriesInClassesFile.push( 145 | createDummyRenderDataEntry( 146 | `export class ${specialization.name}Buffer<${ 147 | parameterObjects.map(renderBufferTemplateParameter).join(", ") 148 | }> extends Uint8Array {}; 149 | `, 150 | ), 151 | ); 152 | }; 153 | 154 | const generateDestructuring = ( 155 | parameterObjects: TemplateParameterData[], 156 | dependencies: Set, 157 | importsInTypesFile: ImportMap, 158 | replaceMap: Map, 159 | ) => { 160 | if (parameterObjects.length === 0) { 161 | return ""; 162 | } else if ( 163 | parameterObjects.length === 1 164 | ) { 165 | const param = parameterObjects[0]; 166 | const type = param.applicationType; 167 | if (type === null) { 168 | return ""; 169 | } else if (isPointer(type) && type.pointee !== "self") { 170 | return `const ${ 171 | renderTypeAsFfi( 172 | dependencies, 173 | importsInTypesFile, 174 | type.pointee, 175 | replaceMap, 176 | ) 177 | } = ${param.name};`; 178 | } else if ( 179 | typeof type === "object" && type 180 | ) { 181 | return `const ${ 182 | renderTypeAsFfi( 183 | dependencies, 184 | importsInTypesFile, 185 | type, 186 | replaceMap, 187 | ) 188 | } = ${param.name};`; 189 | } 190 | } 191 | throw new Error("Unknown template type check kind"); 192 | }; 193 | 194 | interface TemplateParameterData { 195 | name: string; 196 | TypeName: string; 197 | pointerExtends: null | string; 198 | structExtends: null | string; 199 | paramType: TypeEntry; 200 | applicationType: null | TypeEntry; 201 | } 202 | 203 | const generateApplicationParameters = ( 204 | parameters: TemplateParameter[], 205 | application: TypeEntry[], 206 | ): TemplateParameterData[] => { 207 | if (application.length === 0) { 208 | return parameters.map((param) => ({ 209 | name: camelCase(param.name), 210 | TypeName: pascalCase(param.name), 211 | pointerExtends: null, 212 | structExtends: null, 213 | paramType: param, 214 | applicationType: null, 215 | })); 216 | } 217 | if (parameters.length !== application.length) { 218 | throw new Error( 219 | "Unexpected: Different number of template parameters compared to applications", 220 | ); 221 | } 222 | return parameters.map((param, index) => { 223 | const applicationParam = application[index]; 224 | const TypeName = pascalCase(param.name); 225 | const name = camelCase(param.name); 226 | if (typeof applicationParam === "string") { 227 | return { 228 | name, 229 | TypeName, 230 | pointerExtends: `"${applicationParam}"`, 231 | structExtends: `"${applicationParam}"`, 232 | paramType: param, 233 | applicationType: applicationParam, 234 | }; 235 | } else if (applicationParam.kind === "fn") { 236 | // TODO: Handle parameter and return value specialization 237 | return { 238 | name, 239 | TypeName, 240 | pointerExtends: "Deno.UnsafeCallbackDefinition", 241 | structExtends: "Deno.UnsafeCallbackDefinition", 242 | paramType: param, 243 | applicationType: applicationParam, 244 | }; 245 | } else if (applicationParam.kind === "pointer") { 246 | return { 247 | name, 248 | TypeName, 249 | pointerExtends: `"pointer"`, 250 | structExtends: `"pointer"`, 251 | paramType: param, 252 | applicationType: applicationParam, 253 | }; 254 | } else if (applicationParam.kind === "function") { 255 | throw new Error("Unexpected 'function' type parameter"); 256 | } 257 | throw new Error("Unexpected"); 258 | }); 259 | }; 260 | 261 | const renderPointerTemplateParameter = (param: TemplateParameterData): string => 262 | param.pointerExtends 263 | ? `${param.TypeName} extends ${param.pointerExtends}` 264 | : param.TypeName; 265 | const renderBufferTemplateParameter = (param: TemplateParameterData): string => 266 | param.pointerExtends 267 | ? `_${param.TypeName} extends ${param.pointerExtends}` 268 | : `_${param.TypeName}`; 269 | const renderStructTemplateParameter = (param: TemplateParameterData): string => 270 | param.structExtends 271 | ? `const ${param.TypeName} extends ${param.structExtends}` 272 | : `const ${param.TypeName}`; 273 | const renderCallTemplateParameter = (param: TemplateParameterData): string => 274 | `${param.name}: ${param.TypeName}`; 275 | -------------------------------------------------------------------------------- /lib/types.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CXCursor, 3 | CXType, 4 | } from "https://deno.land/x/libclang@1.0.0-beta.8/mod.ts"; 5 | 6 | export type AbsoluteFilePath = `/${string}`; 7 | export type AbsoluteBindingsFilePath = `/${string}.ts`; 8 | export type AbsoluteClassesFilePath = `/${string}.classes.ts`; 9 | export type AbsoluteTypesFilePath = `/${string}.types.ts`; 10 | 11 | export type AbsoluteSystemBindingsFilePath = `/${string}/systemBindings.ts`; 12 | export type AbsoluteSystemClassesFilePath = `/${string}/systemClasses.ts`; 13 | export type AbsoluteSystemTypesFilePath = `/${string}/systemTypes.ts`; 14 | 15 | export type SystemBindingsFileName = "#SYSTEM_B"; 16 | export type SystemClassesFileName = "#SYSTEM_C"; 17 | export type SystemTypesFileName = "#SYSTEM_T"; 18 | export type BindingsFileName = "#FFI"; 19 | 20 | export interface ExportConfiguration { 21 | /** 22 | * Determines the base path against which output files are generated. 23 | * 24 | * @example 25 | * If base path is set to '/path/to/cpp/sources' and a file 26 | * '/path/to/cpp/sources/include/main/header.h' is encountered 27 | * then in the output folder the generated file will be eg. 28 | * 'include/main/header.h.ts'. 29 | * 30 | * Any needed types from files outside the base path get rolled into 31 | * the generic "system" files generated at the root of the output path. 32 | */ 33 | basePath: AbsoluteFilePath; 34 | /** 35 | * Determines the folder into which output files are generated. 36 | */ 37 | outputPath: AbsoluteFilePath; 38 | /** 39 | * Determines which C++ classes and others are considered "entry points" 40 | * into the headers. Only these entries and anything they depend on will 41 | * be generated in the output files. 42 | */ 43 | imports: ImportContent[]; 44 | /** 45 | * Determines which C++ header files are initially included into the build. 46 | * 47 | * Any inclusions these files do will of course be included into the build. 48 | * This list only serves to give a starting point for the build. 49 | */ 50 | files: string[]; 51 | /** 52 | * Determines where Clang will search for included headers. 53 | * 54 | * These get directly passed to Clang as `-I` (include path) parameters. 55 | */ 56 | include: string[]; 57 | } 58 | 59 | export interface ClassContent { 60 | kind: "class"; 61 | name: string; 62 | destructors: boolean; 63 | constructors: boolean | ConstructorFilter; 64 | methods: boolean | string[] | MethodFilter; 65 | contents?: ClassContent[]; 66 | } 67 | 68 | export type ConstructorFilter = (cursor: CXCursor) => boolean; 69 | 70 | export interface MethodContent { 71 | name: string; 72 | } 73 | 74 | export type MethodFilter = (name: string, cursor: CXCursor) => boolean; 75 | 76 | export interface FunctionContent { 77 | kind: "function"; 78 | name: string; 79 | } 80 | 81 | export interface VarContent { 82 | kind: "var"; 83 | name: string; 84 | } 85 | 86 | export type ImportContent = ClassContent | FunctionContent | VarContent; 87 | 88 | export interface BaseEntry { 89 | cursor: CXCursor; 90 | file: AbsoluteFilePath; 91 | name: string; 92 | nsName: string; 93 | used: boolean; 94 | } 95 | 96 | export interface ClassEntry extends BaseEntry { 97 | kind: "class"; 98 | bases: BaseClassEntry[]; 99 | constructors: ClassConstructor[]; 100 | destructor: null | ClassDestructor; 101 | fields: ClassField[]; 102 | forwardDeclarations: CXCursor[]; 103 | methods: ClassMethod[]; 104 | size: number; 105 | virtualBases: BaseClassEntry[]; 106 | usedAsPointer: boolean; 107 | usedAsBuffer: boolean; 108 | } 109 | 110 | export interface ClassConstructor { 111 | parameters: Parameter[]; 112 | cursor: CXCursor; 113 | manglings: string[]; 114 | } 115 | 116 | export interface ClassDestructor { 117 | cursor: CXCursor; 118 | manglings: string[]; 119 | } 120 | 121 | export interface ClassField { 122 | name: string; 123 | cursor: CXCursor; 124 | type: TypeEntry; 125 | } 126 | 127 | export interface ClassMethod { 128 | parameters: Parameter[]; 129 | cursor: CXCursor; 130 | mangling: string; 131 | name: string; 132 | result: null | TypeEntry; 133 | } 134 | 135 | export type BaseClassEntry = 136 | | ClassEntry 137 | | InlineClassTemplateTypeEntry 138 | | TypedefEntry; 139 | 140 | export interface ClassTemplateEntry extends BaseEntry { 141 | defaultSpecialization: null | ClassTemplatePartialSpecialization; 142 | forwardDeclarations: CXCursor[]; 143 | kind: "class"; 144 | parameters: TemplateParameter[]; 145 | partialSpecializations: ClassTemplatePartialSpecialization[]; 146 | } 147 | 148 | export interface ClassTemplatePartialSpecialization { 149 | name: string; 150 | application: TypeEntry[]; 151 | kind: "partial class"; 152 | constructors: ClassConstructor[]; 153 | destructor: null | ClassDestructor; 154 | bases: BaseClassEntry[]; 155 | cursor: CXCursor; 156 | fields: ClassField[]; 157 | methods: ClassMethod[]; 158 | parameters: TemplateParameter[]; 159 | used: boolean; 160 | usedAsPointer: boolean; 161 | usedAsBuffer: boolean; 162 | virtualBases: BaseClassEntry[]; 163 | } 164 | 165 | export interface ClassTemplateConstructor { 166 | parameters: Parameter[]; 167 | cursor: CXCursor; 168 | manglings: string[]; 169 | } 170 | 171 | export interface ClassTemplateDestructor { 172 | cursor: CXCursor; 173 | manglings: string[]; 174 | } 175 | 176 | export interface ClassTemplateField { 177 | name: string; 178 | cursor: CXCursor; 179 | type: TypeEntry; 180 | } 181 | 182 | export interface ClassTemplateMethod { 183 | parameters: Parameter[]; 184 | cursor: CXCursor; 185 | mangling: string; 186 | name: string; 187 | result: null | TypeEntry; 188 | } 189 | 190 | export interface TemplateParameter { 191 | name: string; 192 | kind: ""; 193 | isSpread: boolean; 194 | isRef: boolean; 195 | } 196 | 197 | export interface Parameter { 198 | kind: "parameter"; 199 | comment: null | string; 200 | name: string; 201 | type: TypeEntry; 202 | } 203 | 204 | export interface ConstantArrayTypeEntry { 205 | name?: never; 206 | kind: "[N]"; 207 | length: number; 208 | element: TypeEntry; 209 | type: CXType; 210 | } 211 | 212 | export interface FunctionTypeEntry { 213 | name?: never; 214 | kind: "fn"; 215 | parameters: Parameter[]; 216 | result: null | TypeEntry; 217 | type: CXType; 218 | } 219 | 220 | export interface InlineClassTypeEntry { 221 | base: null | BaseClassEntry; 222 | fields: ClassField[]; 223 | kind: "inline class"; 224 | name?: never; 225 | type: CXType; 226 | } 227 | 228 | export interface InlineClassTemplateTypeEntry { 229 | cursor: CXCursor; 230 | file: AbsoluteFilePath; 231 | kind: "inline class"; 232 | name?: string; 233 | nsName?: string; 234 | parameters: (Parameter | TemplateParameter)[]; 235 | template: ClassTemplateEntry; 236 | specialization: null | ClassTemplatePartialSpecialization; 237 | type: CXType; 238 | } 239 | 240 | export interface InlineUnionTypeEntry { 241 | name?: never; 242 | kind: "inline union"; 243 | fields: ClassField[]; 244 | type: CXType; 245 | } 246 | 247 | export interface MemberPointerTypeEntry { 248 | name?: never; 249 | kind: "member pointer"; 250 | type: CXType; 251 | } 252 | 253 | export interface PointerTypeEntry { 254 | name?: never; 255 | kind: "pointer"; 256 | pointee: "self" | TypeEntry; 257 | type: CXType; 258 | } 259 | 260 | export type PlainTypeString = 261 | | "bool" 262 | | "f32" 263 | | "f64" 264 | | "u8" 265 | | "i8" 266 | | "u16" 267 | | "i16" 268 | | "u32" 269 | | "i32" 270 | | "u64" 271 | | "i64" 272 | | "buffer" 273 | | "pointer" 274 | | "cstring" 275 | | "cstringArray"; 276 | 277 | export type TypeEntry = 278 | | PlainTypeString 279 | | ClassEntry 280 | | ClassTemplateEntry 281 | | FunctionEntry 282 | | EnumEntry 283 | | ConstantArrayTypeEntry 284 | | FunctionTypeEntry 285 | | InlineClassTypeEntry 286 | | InlineClassTemplateTypeEntry 287 | | InlineUnionTypeEntry 288 | | MemberPointerTypeEntry 289 | | PointerTypeEntry 290 | | TemplateParameter 291 | | TypedefEntry 292 | | UnionEntry; 293 | 294 | export interface EnumEntry extends BaseEntry { 295 | kind: "enum"; 296 | type: null | TypeEntry; 297 | } 298 | 299 | export interface FunctionEntry extends BaseEntry { 300 | kind: "function"; 301 | parameters: Parameter[]; 302 | mangling: string; 303 | result: null | TypeEntry; 304 | } 305 | 306 | export interface TypedefEntry extends BaseEntry { 307 | kind: "typedef"; 308 | target: null | TypeEntry; 309 | } 310 | 311 | export interface VarEntry extends BaseEntry { 312 | kind: "var"; 313 | mangling: string; 314 | type: null | TypeEntry; 315 | } 316 | 317 | export interface UnionEntry extends BaseEntry { 318 | kind: "union"; 319 | fields: TypeEntry[]; 320 | } 321 | 322 | export type UseableEntry = 323 | | ClassEntry 324 | | ClassTemplateEntry 325 | | EnumEntry 326 | | FunctionEntry 327 | | TypedefEntry 328 | | UnionEntry 329 | | VarEntry; 330 | 331 | export type ImportMap = Map< 332 | string, 333 | | AbsoluteBindingsFilePath 334 | | AbsoluteClassesFilePath 335 | | AbsoluteTypesFilePath 336 | | SystemBindingsFileName 337 | | SystemClassesFileName 338 | | SystemTypesFileName 339 | | BindingsFileName 340 | >; 341 | 342 | export interface RenderDataEntry { 343 | contents: string; 344 | names: string[]; 345 | dependencies: string[]; 346 | } 347 | 348 | export interface RenderData { 349 | bindings: Set; 350 | bindingsFilePath: AbsoluteBindingsFilePath | AbsoluteSystemBindingsFilePath; 351 | classesFilePath: AbsoluteClassesFilePath | AbsoluteSystemClassesFilePath; 352 | typesFilePath: AbsoluteTypesFilePath | AbsoluteSystemTypesFilePath; 353 | entriesInBindingsFile: RenderDataEntry[]; 354 | entriesInClassesFile: RenderDataEntry[]; 355 | entriesInTypesFile: RenderDataEntry[]; 356 | importsInBindingsFile: ImportMap; 357 | importsInClassesFile: ImportMap; 358 | importsInTypesFile: ImportMap; 359 | } 360 | -------------------------------------------------------------------------------- /lib/renderers/Type/asFfiBindings.ts: -------------------------------------------------------------------------------- 1 | import pascalCase from "https://deno.land/x/case@2.1.1/pascalCase.ts"; 2 | import { ImportMap, TypeEntry } from "../../types.d.ts"; 3 | import { 4 | createSizedStruct, 5 | getSizeOfType, 6 | isFunction, 7 | isPassableByValue, 8 | isPointer, 9 | isStructLike, 10 | SYSTEM_TYPES, 11 | typesFile, 12 | } from "../../utils.ts"; 13 | 14 | const EMPTY_MAP = new Map(); 15 | 16 | export const renderTypeAsFfiBindings = ( 17 | dependencies: Set, 18 | importMap: ImportMap, 19 | type: null | TypeEntry, 20 | templateNameReplaceMap = EMPTY_MAP, 21 | ): string => { 22 | if (type === null) { 23 | return `"void"`; 24 | } else if (typeof type === "string") { 25 | switch (type) { 26 | case "bool": 27 | case "f32": 28 | case "f64": 29 | case "u8": 30 | case "i8": 31 | case "u16": 32 | case "i16": 33 | case "u32": 34 | case "i32": 35 | case "u64": 36 | case "i64": 37 | case "buffer": 38 | case "pointer": 39 | return `"${type}"`; 40 | case "cstring": 41 | case "cstringArray": { 42 | const nameT = `${type}T`; 43 | importMap.set(nameT, SYSTEM_TYPES); 44 | dependencies.add(nameT); 45 | return nameT; 46 | } 47 | default: 48 | throw new Error("Missing switch arm"); 49 | } 50 | } else if ( 51 | type.kind === "enum" 52 | ) { 53 | const name = type.name; 54 | const nameT = `${name}T`; 55 | importMap.set(nameT, typesFile(type.file)); 56 | dependencies.add(nameT); 57 | return nameT; 58 | } else if (type.kind === "class") { 59 | const name = type.name; 60 | const nameT = `${name}T`; 61 | importMap.set(nameT, typesFile(type.file)); 62 | dependencies.add(nameT); 63 | return nameT; 64 | } else if (type.kind === "typedef") { 65 | const name = type.name; 66 | const nameT = `${name}T`; 67 | importMap.set(nameT, typesFile(type.file)); 68 | dependencies.add(nameT); 69 | if ( 70 | isFunction(type.target) || 71 | isPointer(type.target) && isFunction(type.target.pointee) 72 | ) { 73 | importMap.set("func", SYSTEM_TYPES); 74 | return `func(${nameT})`; 75 | } 76 | 77 | return nameT; 78 | } else if (type.kind === "pointer") { 79 | return renderPointerAsFfi( 80 | dependencies, 81 | importMap, 82 | type.pointee, 83 | templateNameReplaceMap, 84 | ); 85 | } else if (type.kind === "function" || type.kind === "fn") { 86 | const parametersStrings = type.parameters.map((param) => 87 | renderTypeAsFfiBindings( 88 | dependencies, 89 | importMap, 90 | param.type, 91 | templateNameReplaceMap, 92 | ) 93 | ) 94 | .join(", "); 95 | if (type.parameters.length === 1 && parametersStrings.startsWith("...")) { 96 | return `{ parameters: ${parametersStrings.substring(3)}, result: ${ 97 | renderTypeAsFfiBindings( 98 | dependencies, 99 | importMap, 100 | type.result, 101 | templateNameReplaceMap, 102 | ) 103 | } }`; 104 | } 105 | return `{ parameters: [${parametersStrings}], result: ${ 106 | renderTypeAsFfiBindings( 107 | dependencies, 108 | importMap, 109 | type.result, 110 | templateNameReplaceMap, 111 | ) 112 | } }`; 113 | } else if (type.kind === "inline class") { 114 | const templateT = `${type.template.name}T`; 115 | importMap.set(templateT, typesFile(type.template.file)); 116 | dependencies.add(templateT); 117 | return `${templateT}(${ 118 | (type.parameters.map((param) => 119 | param.kind === "parameter" 120 | ? renderTypeAsFfiBindings( 121 | dependencies, 122 | importMap, 123 | param.type, 124 | templateNameReplaceMap, 125 | ) 126 | : renderTypeAsFfiBindings( 127 | dependencies, 128 | importMap, 129 | param, 130 | templateNameReplaceMap, 131 | ) 132 | )) 133 | .join( 134 | ", ", 135 | ) 136 | })`; 137 | } else if (type.kind === "inline class") { 138 | return `{ struct: [${ 139 | (type.base 140 | ? [ 141 | renderTypeAsFfiBindings( 142 | dependencies, 143 | importMap, 144 | type.base, 145 | templateNameReplaceMap, 146 | ), 147 | ] 148 | : []) 149 | .concat( 150 | type.fields.map((field) => 151 | renderTypeAsFfiBindings( 152 | dependencies, 153 | importMap, 154 | field.type, 155 | templateNameReplaceMap, 156 | ) 157 | ), 158 | ).join( 159 | ", ", 160 | ) 161 | }] }`; 162 | } else if (type.kind === "[N]") { 163 | const fieldString = renderTypeAsFfiBindings( 164 | dependencies, 165 | importMap, 166 | type.element, 167 | templateNameReplaceMap, 168 | ); 169 | return `{ struct: [${ 170 | new Array(type.length).fill(fieldString).join(",") 171 | }] }`; 172 | } else if (type.kind === "inline union") { 173 | const uniqueSortedFields = [ 174 | ...new Set( 175 | type.fields.sort((a, b) => 176 | getSizeOfType(b.type) - getSizeOfType(a.type) 177 | ).map((field) => 178 | renderTypeAsFfiBindings( 179 | dependencies, 180 | importMap, 181 | field.type, 182 | templateNameReplaceMap, 183 | ) 184 | ), 185 | ), 186 | ]; 187 | const count = uniqueSortedFields.length; 188 | importMap.set(`union${count}`, SYSTEM_TYPES); 189 | return `union${count}(${ 190 | uniqueSortedFields.join( 191 | ", ", 192 | ) 193 | })`; 194 | } else if (type.kind === "member pointer") { 195 | return JSON.stringify(createSizedStruct(type.type)); 196 | } else if (type.kind === "class") { 197 | const nameT = `${type.name}T`; 198 | importMap.set(nameT, typesFile(type.file)); 199 | dependencies.add(nameT); 200 | return nameT; 201 | } else if (type.kind === "union") { 202 | const name = type.name; 203 | const nameT = `${name}T`; 204 | importMap.set(nameT, typesFile(type.file)); 205 | dependencies.add(nameT); 206 | 207 | return nameT; 208 | } else if (type.kind === "") { 209 | if (type.isRef) { 210 | importMap.set("ptr", SYSTEM_TYPES); 211 | } 212 | const TypeName = templateNameReplaceMap.get(type.name) || 213 | pascalCase(type.name); 214 | if (type.isSpread && type.isRef) { 215 | return `...${TypeName}.map(ptr)`; 216 | } else if (type.isSpread) { 217 | return `...${TypeName}`; 218 | } else if (type.isRef) { 219 | return `ptr(${TypeName})`; 220 | } 221 | return TypeName; 222 | } else { 223 | throw new Error( 224 | // @ts-expect-error kind and name will exist in any added TypeEntry types 225 | "internal error: unknown type kind: " + type.kind + ": " + type.name, 226 | ); 227 | } 228 | }; 229 | 230 | const renderPointerAsFfi = ( 231 | dependencies: Set, 232 | importMap: ImportMap, 233 | pointee: "self" | TypeEntry, 234 | templateNameReplaceMap = EMPTY_MAP, 235 | ) => { 236 | if (typeof pointee === "string") { 237 | // Primitive value pointers are usually out pointers. 238 | importMap.set("buf", SYSTEM_TYPES); 239 | if (pointee === "self") { 240 | return `buf("self")`; 241 | } 242 | return `buf(${(renderTypeAsFfiBindings( 243 | dependencies, 244 | importMap, 245 | pointee, 246 | templateNameReplaceMap, 247 | ))})`; 248 | } else if (pointee.kind === "pointer") { 249 | // Pointer to pointer is usually an out pointer. 250 | importMap.set("buf", SYSTEM_TYPES); 251 | return `buf(${(renderTypeAsFfiBindings( 252 | dependencies, 253 | importMap, 254 | pointee, 255 | templateNameReplaceMap, 256 | ))})`; 257 | } else if (pointee.kind === "inline class") { 258 | if (!pointee.specialization) { 259 | pointee.specialization = pointee.template.defaultSpecialization!; 260 | } 261 | if ( 262 | !pointee.specialization.usedAsBuffer && 263 | pointee.specialization.usedAsPointer 264 | ) { 265 | // Class template seen only as a pointer should use 266 | // pointers as FFI interface type. 267 | importMap.set("ptr", SYSTEM_TYPES); 268 | return `ptr(${(renderTypeAsFfiBindings( 269 | dependencies, 270 | importMap, 271 | pointee, 272 | templateNameReplaceMap, 273 | ))})`; 274 | } 275 | importMap.set("buf", SYSTEM_TYPES); 276 | return `buf(${ 277 | renderTypeAsFfiBindings( 278 | dependencies, 279 | importMap, 280 | pointee, 281 | templateNameReplaceMap, 282 | ) 283 | })`; 284 | } else if ( 285 | pointee.kind === "class" 286 | ) { 287 | if ( 288 | !pointee.usedAsBuffer && 289 | pointee.usedAsPointer 290 | ) { 291 | // Class seen only as a pointer should use pointers as 292 | // FFI interface type. 293 | importMap.set("ptr", SYSTEM_TYPES); 294 | return `ptr(${(renderTypeAsFfiBindings( 295 | dependencies, 296 | importMap, 297 | pointee, 298 | templateNameReplaceMap, 299 | ))})`; 300 | } 301 | importMap.set("buf", SYSTEM_TYPES); 302 | return `buf(${(renderTypeAsFfiBindings( 303 | dependencies, 304 | importMap, 305 | pointee, 306 | templateNameReplaceMap, 307 | ))})`; 308 | } else if (pointee.kind === "function" || pointee.kind === "fn") { 309 | // Function pointer is just a function. 310 | return `func(${ 311 | renderTypeAsFfiBindings( 312 | dependencies, 313 | importMap, 314 | pointee, 315 | templateNameReplaceMap, 316 | ) 317 | })`; 318 | } else if (pointee.kind === "typedef") { 319 | const passByValue = isPassableByValue(pointee); 320 | if (passByValue) { 321 | importMap.set("buf", SYSTEM_TYPES); 322 | } else { 323 | importMap.set("ptr", SYSTEM_TYPES); 324 | } 325 | return passByValue 326 | ? `buf(${(renderTypeAsFfiBindings( 327 | dependencies, 328 | importMap, 329 | pointee, 330 | templateNameReplaceMap, 331 | ))})` 332 | : `ptr(${(renderTypeAsFfiBindings( 333 | dependencies, 334 | importMap, 335 | pointee, 336 | templateNameReplaceMap, 337 | ))})`; 338 | } else if (pointee.kind === "enum") { 339 | importMap.set("buf", SYSTEM_TYPES); 340 | return `buf(${(renderTypeAsFfiBindings( 341 | dependencies, 342 | importMap, 343 | pointee, 344 | templateNameReplaceMap, 345 | ))})`; 346 | } else if ( 347 | pointee.kind === "inline class" || pointee.kind === "[N]" 348 | ) { 349 | if (isPassableByValue(pointee)) { 350 | return renderTypeAsFfiBindings( 351 | dependencies, 352 | importMap, 353 | "pointer", 354 | templateNameReplaceMap, 355 | ); 356 | } else { 357 | return renderTypeAsFfiBindings( 358 | dependencies, 359 | importMap, 360 | "buffer", 361 | templateNameReplaceMap, 362 | ); 363 | } 364 | } else if (isStructLike(pointee)) { 365 | importMap.set("buf", SYSTEM_TYPES); 366 | return `buf(${(renderTypeAsFfiBindings( 367 | dependencies, 368 | importMap, 369 | pointee, 370 | templateNameReplaceMap, 371 | ))})`; 372 | } else { 373 | importMap.set("ptr", SYSTEM_TYPES); 374 | return `ptr(${(renderTypeAsFfiBindings( 375 | dependencies, 376 | importMap, 377 | pointee, 378 | templateNameReplaceMap, 379 | ))})`; 380 | } 381 | }; 382 | -------------------------------------------------------------------------------- /lib/renderers/Type/asFfiBindingTypes.ts: -------------------------------------------------------------------------------- 1 | import pascalCase from "https://deno.land/x/case@2.1.1/pascalCase.ts"; 2 | import { ImportMap, TypeEntry } from "../../types.d.ts"; 3 | import { 4 | createSizedStruct, 5 | getSizeOfType, 6 | isFunction, 7 | isPassableByValue, 8 | isPointer, 9 | isStructLike, 10 | SYSTEM_TYPES, 11 | typesFile, 12 | } from "../../utils.ts"; 13 | 14 | const EMPTY_MAP = new Map(); 15 | 16 | export const renderTypeAsFfiBindingTypes = ( 17 | dependencies: Set, 18 | importMap: ImportMap, 19 | type: null | TypeEntry, 20 | templateNameReplaceMap = EMPTY_MAP, 21 | ): string => { 22 | if (type === null) { 23 | return `"void"`; 24 | } else if (typeof type === "string") { 25 | switch (type) { 26 | case "bool": 27 | case "f32": 28 | case "f64": 29 | case "u8": 30 | case "i8": 31 | case "u16": 32 | case "i16": 33 | case "u32": 34 | case "i32": 35 | case "u64": 36 | case "i64": 37 | case "buffer": 38 | case "pointer": 39 | return `"${type}"`; 40 | case "cstring": 41 | case "cstringArray": { 42 | const nameT = `${type}T`; 43 | importMap.set(`type ${nameT}`, SYSTEM_TYPES); 44 | dependencies.add(nameT); 45 | return `typeof ${nameT}`; 46 | } 47 | default: 48 | throw new Error("Missing switch arm"); 49 | } 50 | } else if ( 51 | type.kind === "enum" 52 | ) { 53 | const name = type.name; 54 | const nameT = `${name}T`; 55 | importMap.set(`type ${nameT}`, typesFile(type.file)); 56 | dependencies.add(nameT); 57 | return `typeof ${nameT}`; 58 | } else if (type.kind === "class") { 59 | const name = type.name; 60 | const nameT = `${name}Pointer`; 61 | importMap.set(`type ${nameT}`, typesFile(type.file)); 62 | dependencies.add(nameT); 63 | return `${nameT}`; 64 | } else if (type.kind === "typedef") { 65 | const name = type.name; 66 | const nameT = `${name}T`; 67 | importMap.set(nameT, typesFile(type.file)); 68 | dependencies.add(nameT); 69 | if ( 70 | isFunction(type.target) || 71 | isPointer(type.target) && isFunction(type.target.pointee) 72 | ) { 73 | importMap.set("Func", SYSTEM_TYPES); 74 | return `Func`; 75 | } 76 | 77 | return nameT; 78 | } else if (type.kind === "pointer") { 79 | return renderPointerAsFfiBindingTypes( 80 | dependencies, 81 | importMap, 82 | type.pointee, 83 | templateNameReplaceMap, 84 | ); 85 | } else if (type.kind === "function" || type.kind === "fn") { 86 | const parametersStrings = type.parameters.map((param) => 87 | renderTypeAsFfiBindingTypes( 88 | dependencies, 89 | importMap, 90 | param.type, 91 | templateNameReplaceMap, 92 | ) 93 | ) 94 | .join(", "); 95 | if (type.parameters.length === 1 && parametersStrings.startsWith("...")) { 96 | return `Deno.UnsafeCallbackDefinition<[${ 97 | parametersStrings.substring(3) 98 | }], ${ 99 | renderTypeAsFfiBindingTypes( 100 | dependencies, 101 | importMap, 102 | type.result, 103 | templateNameReplaceMap, 104 | ) 105 | }>`; 106 | } 107 | return `Deno.UnsafeCallbackDefinition<[${parametersStrings}], ${ 108 | renderTypeAsFfiBindingTypes( 109 | dependencies, 110 | importMap, 111 | type.result, 112 | templateNameReplaceMap, 113 | ) 114 | }>`; 115 | } else if (type.kind === "inline class") { 116 | const templateT = `${type.template.name}T`; 117 | importMap.set(templateT, typesFile(type.template.file)); 118 | dependencies.add(templateT); 119 | return `${templateT}(${ 120 | (type.parameters.map((param) => 121 | param.kind === "parameter" 122 | ? renderTypeAsFfiBindingTypes( 123 | dependencies, 124 | importMap, 125 | param.type, 126 | templateNameReplaceMap, 127 | ) 128 | : param.name 129 | )) 130 | .join( 131 | ", ", 132 | ) 133 | })`; 134 | } else if (type.kind === "inline class") { 135 | return `{ struct: [${ 136 | (type.base 137 | ? [ 138 | renderTypeAsFfiBindingTypes( 139 | dependencies, 140 | importMap, 141 | type.base, 142 | templateNameReplaceMap, 143 | ), 144 | ] 145 | : []) 146 | .concat( 147 | type.fields.map((field) => 148 | renderTypeAsFfiBindingTypes( 149 | dependencies, 150 | importMap, 151 | field.type, 152 | templateNameReplaceMap, 153 | ) 154 | ), 155 | ).join( 156 | ", ", 157 | ) 158 | }] }`; 159 | } else if (type.kind === "[N]") { 160 | const fieldString = renderTypeAsFfiBindingTypes( 161 | dependencies, 162 | importMap, 163 | type.element, 164 | templateNameReplaceMap, 165 | ); 166 | return `{ struct: [${ 167 | new Array(type.length).fill(fieldString).join(",") 168 | }] }`; 169 | } else if (type.kind === "inline union") { 170 | const uniqueSortedFields = [ 171 | ...new Set( 172 | type.fields.sort((a, b) => 173 | getSizeOfType(b.type) - getSizeOfType(a.type) 174 | ).map((field) => 175 | renderTypeAsFfiBindingTypes( 176 | dependencies, 177 | importMap, 178 | field.type, 179 | templateNameReplaceMap, 180 | ) 181 | ), 182 | ), 183 | ]; 184 | return uniqueSortedFields.join( 185 | " | ", 186 | ); 187 | } else if (type.kind === "member pointer") { 188 | return JSON.stringify(createSizedStruct(type.type)); 189 | } else if (type.kind === "class") { 190 | const nameT = `${type.name}Pointer`; 191 | importMap.set(`type ${nameT}`, typesFile(type.file)); 192 | dependencies.add(nameT); 193 | return nameT; 194 | } else if (type.kind === "union") { 195 | const name = type.name; 196 | const nameT = `${name}T`; 197 | importMap.set(`type ${nameT}`, typesFile(type.file)); 198 | dependencies.add(nameT); 199 | 200 | return nameT; 201 | } else if (type.kind === "") { 202 | if (type.isRef) { 203 | importMap.set("type Ptr", SYSTEM_TYPES); 204 | } 205 | const TypeName = templateNameReplaceMap.get(type.name) || 206 | pascalCase(type.name); 207 | if (type.isSpread && type.isRef) { 208 | return `...Ptr<${TypeName}>[]`; 209 | } else if (type.isSpread) { 210 | return `...${TypeName}`; 211 | } else if (type.isRef) { 212 | return `Ptr<${TypeName}>`; 213 | } 214 | return TypeName; 215 | } else { 216 | throw new Error( 217 | // @ts-expect-error kind and name will exist in any added TypeEntry types 218 | "internal error: unknown type kind: " + type.kind + ": " + type.name, 219 | ); 220 | } 221 | }; 222 | 223 | const renderPointerAsFfiBindingTypes = ( 224 | dependencies: Set, 225 | importMap: ImportMap, 226 | pointee: "self" | TypeEntry, 227 | templateNameReplaceMap = EMPTY_MAP, 228 | ) => { 229 | if (typeof pointee === "string") { 230 | // Primitive value pointers are usually out pointers. 231 | importMap.set("type Buf", SYSTEM_TYPES); 232 | if (pointee === "self") { 233 | return `Buf<"self">`; 234 | } 235 | return `Buf<${(renderTypeAsFfiBindingTypes( 236 | dependencies, 237 | importMap, 238 | pointee, 239 | templateNameReplaceMap, 240 | ))}>`; 241 | } else if (pointee.kind === "pointer") { 242 | // Pointer to pointer is usually an out pointer. 243 | importMap.set("type Buf", SYSTEM_TYPES); 244 | return `Buf<${(renderTypeAsFfiBindingTypes( 245 | dependencies, 246 | importMap, 247 | pointee, 248 | templateNameReplaceMap, 249 | ))}>`; 250 | } else if (pointee.kind === "inline class") { 251 | if (!pointee.specialization) { 252 | pointee.specialization = pointee.template.defaultSpecialization!; 253 | } 254 | if ( 255 | !pointee.specialization.usedAsBuffer && 256 | pointee.specialization.usedAsPointer 257 | ) { 258 | // Class template seen only as a pointer should use 259 | // pointers as FFI interface type. 260 | importMap.set("type Ptr", SYSTEM_TYPES); 261 | return `Ptr<${(renderTypeAsFfiBindingTypes( 262 | dependencies, 263 | importMap, 264 | pointee, 265 | templateNameReplaceMap, 266 | ))}>`; 267 | } 268 | importMap.set("type Buf", SYSTEM_TYPES); 269 | return `Buf<${ 270 | renderTypeAsFfiBindingTypes( 271 | dependencies, 272 | importMap, 273 | pointee, 274 | templateNameReplaceMap, 275 | ) 276 | }>`; 277 | } else if ( 278 | pointee.kind === "class" 279 | ) { 280 | if ( 281 | !pointee.usedAsBuffer && 282 | pointee.usedAsPointer 283 | ) { 284 | // Class seen only as a pointer should use pointers as 285 | // FFI interface type. 286 | importMap.set("type Ptr", SYSTEM_TYPES); 287 | return `Ptr<${(renderTypeAsFfiBindingTypes( 288 | dependencies, 289 | importMap, 290 | pointee, 291 | templateNameReplaceMap, 292 | ))}>`; 293 | } 294 | importMap.set("type Buf", SYSTEM_TYPES); 295 | return `Buf<${(renderTypeAsFfiBindingTypes( 296 | dependencies, 297 | importMap, 298 | pointee, 299 | templateNameReplaceMap, 300 | ))}>`; 301 | } else if (pointee.kind === "function" || pointee.kind === "fn") { 302 | // Function pointer is just a function. 303 | return `Func<${ 304 | renderTypeAsFfiBindingTypes( 305 | dependencies, 306 | importMap, 307 | pointee, 308 | templateNameReplaceMap, 309 | ) 310 | }>`; 311 | } else if (pointee.kind === "typedef") { 312 | const passByValue = isPassableByValue(pointee); 313 | if (passByValue) { 314 | importMap.set("type Buf", SYSTEM_TYPES); 315 | } else { 316 | importMap.set("type Ptr", SYSTEM_TYPES); 317 | } 318 | return passByValue 319 | ? `Buf<${(renderTypeAsFfiBindingTypes( 320 | dependencies, 321 | importMap, 322 | pointee, 323 | templateNameReplaceMap, 324 | ))}>` 325 | : `Ptr<${(renderTypeAsFfiBindingTypes( 326 | dependencies, 327 | importMap, 328 | pointee, 329 | templateNameReplaceMap, 330 | ))}>`; 331 | } else if (pointee.kind === "enum") { 332 | importMap.set("type Buf", SYSTEM_TYPES); 333 | return `Buf<${(renderTypeAsFfiBindingTypes( 334 | dependencies, 335 | importMap, 336 | pointee, 337 | templateNameReplaceMap, 338 | ))}>`; 339 | } else if ( 340 | pointee.kind === "inline class" || pointee.kind === "[N]" 341 | ) { 342 | if (isPassableByValue(pointee)) { 343 | return renderTypeAsFfiBindingTypes( 344 | dependencies, 345 | importMap, 346 | "pointer", 347 | templateNameReplaceMap, 348 | ); 349 | } else { 350 | return renderTypeAsFfiBindingTypes( 351 | dependencies, 352 | importMap, 353 | "buffer", 354 | templateNameReplaceMap, 355 | ); 356 | } 357 | } else if (isStructLike(pointee)) { 358 | importMap.set("type Buf", SYSTEM_TYPES); 359 | return `Buf<${(renderTypeAsFfiBindingTypes( 360 | dependencies, 361 | importMap, 362 | pointee, 363 | templateNameReplaceMap, 364 | ))}>`; 365 | } else { 366 | importMap.set("type Ptr", SYSTEM_TYPES); 367 | return `Ptr<${(renderTypeAsFfiBindingTypes( 368 | dependencies, 369 | importMap, 370 | pointee, 371 | templateNameReplaceMap, 372 | ))}>`; 373 | } 374 | }; 375 | -------------------------------------------------------------------------------- /lib/renderers/Typedef.ts: -------------------------------------------------------------------------------- 1 | import constantCase from "https://deno.land/x/case@2.1.1/constantCase.ts"; 2 | import { 3 | ClassEntry, 4 | ConstantArrayTypeEntry, 5 | FunctionEntry, 6 | FunctionTypeEntry, 7 | InlineClassTemplateTypeEntry, 8 | InlineClassTypeEntry, 9 | PlainTypeString, 10 | RenderData, 11 | TypedefEntry, 12 | } from "../types.d.ts"; 13 | import { 14 | classesFile, 15 | createRenderDataEntry, 16 | isConstantArray, 17 | isFunction, 18 | isInlineStruct, 19 | isInlineTemplateStruct, 20 | isPointer, 21 | isStruct, 22 | isStructLike, 23 | isTypedef, 24 | typesFile, 25 | } from "../utils.ts"; 26 | import { 27 | renderTypeAsFfi, 28 | renderTypeAsFfiBindingTypes, 29 | renderTypeAsTS, 30 | } from "./Type.ts"; 31 | import { renderClassBufferConstructor } from "./Class.ts"; 32 | 33 | export const renderTypedef = ( 34 | renderData: RenderData, 35 | entry: TypedefEntry, 36 | ) => { 37 | const { 38 | name, 39 | target, 40 | } = entry; 41 | if (!target) { 42 | throw new Error("Void typedef"); 43 | } 44 | 45 | const { 46 | entriesInTypesFile, 47 | importsInTypesFile, 48 | } = renderData; 49 | 50 | const nameT = `${name}T`; 51 | const dependencies = new Set(); 52 | if (typeof target === "string") { 53 | return renderStaticTarget(renderData, name, target); 54 | } 55 | if ( 56 | target.name === name && 57 | "file" in target && target.file === entry.file 58 | ) { 59 | /** 60 | * De-namespacing: 61 | * ```cpp 62 | * namespace Internal { 63 | * enum Foo { 64 | * Bar, 65 | * }; 66 | * } 67 | * using Foo = Internal::Foo; 68 | * ``` 69 | */ 70 | return; 71 | } 72 | 73 | switch (target.kind) { 74 | case "function": 75 | renderFunctionTarget(renderData, name, target); 76 | return; 77 | case "pointer": 78 | if (isFunction(target.pointee)) { 79 | renderFunctionTarget(renderData, name, target.pointee); 80 | return; 81 | } 82 | break; 83 | case "typedef": 84 | renderTypedefTarget(renderData, name, target); 85 | return; 86 | case "class": 87 | case "class": 88 | case "enum": 89 | case "[N]": 90 | case "fn": 91 | case "inline class": 92 | case "inline class": 93 | case "inline union": 94 | case "member pointer": 95 | case "union": 96 | } 97 | if (isTypedef(target)) { 98 | return renderTypedefTarget(renderData, name, target); 99 | } else if ( 100 | isInlineTemplateStruct(target) 101 | ) { 102 | return renderInlineTemplateTarget(renderData, name, target); 103 | } else if ( 104 | isStruct(target) 105 | ) { 106 | return renderStruct(renderData, name, target); 107 | } else if ( 108 | (isInlineStruct(target) || 109 | isConstantArray(target)) && 110 | entry.cursor.getType()!.getSizeOf() > 0 111 | ) { 112 | return renderInlineStructOrConstantArray(renderData, name, target); 113 | } else if (isFunction(target)) { 114 | return renderFunctionTarget(renderData, name, target); 115 | } else if (isPointer(target) && isFunction(target.pointee)) { 116 | return renderFunctionTarget(renderData, name, target.pointee); 117 | } 118 | 119 | const refT = renderTypeAsFfi( 120 | dependencies, 121 | importsInTypesFile, 122 | isPointer(target) && isFunction(target.pointee) ? target.pointee : target, 123 | ); 124 | 125 | const ref = renderTypeAsTS(dependencies, importsInTypesFile, target); 126 | const typesDefinition = `export const ${nameT} = ${refT}${ 127 | refT.endsWith("}") ? " as const" : "" 128 | }; 129 | export type ${name} = ${ref}; 130 | `; 131 | entriesInTypesFile.push( 132 | createRenderDataEntry([nameT, name], [...dependencies], typesDefinition), 133 | ); 134 | }; 135 | 136 | const renderStaticTarget = ( 137 | { entriesInTypesFile, importsInTypesFile }: RenderData, 138 | name: string, 139 | target: PlainTypeString, 140 | ) => { 141 | const nameT = `${name}T`; 142 | const dependencies = new Set(); 143 | const typesDefinition = `export const ${nameT} = ${ 144 | renderTypeAsFfi(dependencies, importsInTypesFile, target) 145 | }; 146 | export type ${name} = ${ 147 | renderTypeAsTS(dependencies, importsInTypesFile, target) 148 | }; 149 | `; 150 | entriesInTypesFile.push( 151 | createRenderDataEntry([nameT, name], [...dependencies], typesDefinition), 152 | ); 153 | }; 154 | 155 | const renderTypedefTarget = ( 156 | { 157 | entriesInClassesFile, 158 | entriesInTypesFile, 159 | importsInClassesFile, 160 | importsInTypesFile, 161 | }: RenderData, 162 | name: string, 163 | target: TypedefEntry, 164 | ) => { 165 | const nameT = `${name}T`; 166 | const targetName = target.name; 167 | const nameBuffer = `${name}Buffer`; 168 | const namePointer = `${name}Pointer`; 169 | const targetBuffer = `${targetName}Buffer`; 170 | const targetPointer = `${targetName}Pointer`; 171 | const targetT = `${targetName}T`; 172 | if (isStructLike(target.target)) { 173 | importsInClassesFile.set( 174 | targetBuffer, 175 | classesFile(target.file), 176 | ); 177 | importsInTypesFile.set( 178 | `type ${targetPointer}`, 179 | typesFile(target.file), 180 | ); 181 | importsInTypesFile.set( 182 | targetT, 183 | typesFile(target.file), 184 | ); 185 | entriesInClassesFile.push( 186 | createRenderDataEntry( 187 | [nameBuffer], 188 | [targetBuffer], 189 | `const ${nameBuffer} = ${targetBuffer}; 190 | export { ${targetBuffer} as ${nameBuffer} }; 191 | `, 192 | ), 193 | ); 194 | entriesInTypesFile.push( 195 | createRenderDataEntry( 196 | [nameT, namePointer], 197 | [targetT, targetPointer], 198 | `export const ${nameT} = ${targetT}; 199 | export type ${namePointer} = ${targetPointer}; 200 | `, 201 | ), 202 | ); 203 | } else { 204 | importsInTypesFile.set( 205 | `type ${targetName}`, 206 | typesFile(target.file), 207 | ); 208 | importsInTypesFile.set( 209 | targetT, 210 | typesFile(target.file), 211 | ); 212 | entriesInTypesFile.push( 213 | createRenderDataEntry( 214 | [nameT, name], 215 | [targetT, targetName], 216 | `export const ${nameT} = ${targetT}; 217 | export type ${name} = ${targetName}; 218 | `, 219 | ), 220 | ); 221 | } 222 | }; 223 | 224 | const renderInlineTemplateTarget = ( 225 | { 226 | entriesInClassesFile, 227 | entriesInTypesFile, 228 | importsInClassesFile, 229 | importsInTypesFile, 230 | typesFilePath, 231 | }: RenderData, 232 | name: string, 233 | target: InlineClassTemplateTypeEntry, 234 | ) => { 235 | const nameT = `${name}T`; 236 | const namePointer = `${name}Pointer`; 237 | const dependencies = new Set(); 238 | const BUFFER_SIZE = `${constantCase(name)}_SIZE` as const; 239 | importsInClassesFile.set(BUFFER_SIZE, typesFilePath); 240 | const BaseBuffer = renderTypeAsTS(dependencies, importsInClassesFile, target); 241 | const nameBuffer = `${name}Buffer` as const; 242 | const classesEntry = createRenderDataEntry( 243 | [nameBuffer], 244 | [], 245 | `export class ${nameBuffer} extends ${BaseBuffer} { 246 | ${renderClassBufferConstructor(nameBuffer, BUFFER_SIZE)} 247 | } 248 | `, 249 | ); 250 | entriesInClassesFile.push( 251 | classesEntry, 252 | ); 253 | const asConst = isInlineTemplateStruct(target) ? "" : " as const"; 254 | const NAME_SIZE = `${constantCase(name)}_SIZE`; 255 | const refT = renderTypeAsFfi( 256 | dependencies, 257 | importsInTypesFile, 258 | target, 259 | ); 260 | const targetName = target.name; 261 | const targetPointer = `${targetName}Pointer`; 262 | dependencies.add(targetPointer); 263 | importsInTypesFile.set(`type ${targetPointer}`, typesFile(target.file)); 264 | const typesEntry = createRenderDataEntry( 265 | [NAME_SIZE, nameT, namePointer], 266 | [...dependencies], 267 | `export const ${NAME_SIZE} = ${ 268 | target.cursor.getType()!.getSizeOf() 269 | } as const; 270 | export const ${nameT} = ${refT}${asConst}; 271 | export type ${namePointer} = ${targetPointer}<${ 272 | target.parameters.map((param) => 273 | param.kind === "parameter" 274 | ? renderTypeAsFfiBindingTypes( 275 | dependencies, 276 | importsInTypesFile, 277 | param.type, 278 | ) 279 | : param.name 280 | ).join(", ") 281 | }>; 282 | `, 283 | ); 284 | entriesInTypesFile.push(typesEntry); 285 | return; 286 | }; 287 | 288 | const renderStruct = ( 289 | { 290 | entriesInClassesFile, 291 | entriesInTypesFile, 292 | importsInClassesFile, 293 | importsInTypesFile, 294 | }: RenderData, 295 | name: string, 296 | target: ClassEntry, 297 | ) => { 298 | const nameT = `${name}T`; 299 | const targetName = target.name; 300 | const nameBuffer = `${name}Buffer`; 301 | const namePointer = `${name}Pointer`; 302 | const targetBuffer = `${targetName}Buffer`; 303 | const targetPointer = `${targetName}Pointer`; 304 | const targetT = `${targetName}T`; 305 | importsInClassesFile.set( 306 | targetBuffer, 307 | classesFile(target.file), 308 | ); 309 | importsInTypesFile.set( 310 | targetT, 311 | typesFile(target.file), 312 | ); 313 | importsInTypesFile.set( 314 | targetPointer, 315 | typesFile(target.file), 316 | ); 317 | entriesInTypesFile.push( 318 | createRenderDataEntry( 319 | [nameT, namePointer], 320 | [targetT, targetPointer], 321 | `export const ${nameT} = ${targetT}; 322 | export type ${namePointer} = ${targetPointer}; 323 | `, 324 | ), 325 | ); 326 | entriesInClassesFile.push( 327 | createRenderDataEntry( 328 | [nameBuffer], 329 | [targetBuffer], 330 | `const ${nameBuffer} = ${targetBuffer}; 331 | export { ${targetBuffer} as ${nameBuffer} }; 332 | `, 333 | ), 334 | ); 335 | }; 336 | 337 | const renderInlineStructOrConstantArray = ( 338 | { 339 | entriesInClassesFile, 340 | entriesInTypesFile, 341 | importsInClassesFile, 342 | importsInTypesFile, 343 | typesFilePath, 344 | }: RenderData, 345 | name: string, 346 | target: InlineClassTypeEntry | ConstantArrayTypeEntry, 347 | ) => { 348 | const nameT = `${name}T`; 349 | const nameBuffer = `${name}Buffer` as const; 350 | const namePointer = `${name}Pointer`; 351 | const dependencies = new Set(); 352 | const BUFFER_SIZE = `${constantCase(name)}_SIZE` as const; 353 | importsInClassesFile.set(BUFFER_SIZE, typesFilePath); 354 | const classesEntry = createRenderDataEntry( 355 | [nameBuffer], 356 | [], 357 | `export class ${nameBuffer} extends Uint8Array { 358 | ${renderClassBufferConstructor(nameBuffer, BUFFER_SIZE)} 359 | } 360 | `, 361 | ); 362 | entriesInClassesFile.push( 363 | classesEntry, 364 | ); 365 | const refT = renderTypeAsFfi( 366 | dependencies, 367 | importsInTypesFile, 368 | target, 369 | ); 370 | const asConst = isInlineTemplateStruct(target) ? "" : " as const"; 371 | const NAME_SIZE = `${constantCase(name)}_SIZE`; 372 | const typesEntry = createRenderDataEntry( 373 | [NAME_SIZE, nameT, namePointer], 374 | [...dependencies], 375 | `export const ${NAME_SIZE} = ${target.type.getSizeOf()} as const; 376 | export const ${nameT} = ${refT}${asConst}; 377 | declare const ${name}: unique symbol; 378 | export type ${namePointer} = NonNullable & { [${name}]: unknown }; 379 | `, 380 | ); 381 | entriesInTypesFile.push(typesEntry); 382 | return; 383 | }; 384 | 385 | const renderFunctionTarget = ( 386 | { 387 | entriesInTypesFile, 388 | importsInTypesFile, 389 | }: RenderData, 390 | name: string, 391 | target: FunctionEntry | FunctionTypeEntry, 392 | ) => { 393 | const nameT = `${name}T`; 394 | const dependencies = new Set(); 395 | const refT = renderTypeAsFfi(dependencies, importsInTypesFile, target); 396 | entriesInTypesFile.push( 397 | createRenderDataEntry( 398 | [nameT, name], 399 | [...dependencies], 400 | `export const ${nameT} = ${refT} as const; 401 | declare const ${name}_: unique symbol; 402 | export type ${name} = NonNullable & { [${name}_]: unknown }; 403 | `, 404 | ), 405 | ); 406 | }; 407 | -------------------------------------------------------------------------------- /lib/visitors/Class.ts: -------------------------------------------------------------------------------- 1 | import { CXTypeKind } from "https://deno.land/x/libclang@1.0.0-beta.8/include/typeDefinitions.ts"; 2 | import { 3 | CX_CXXAccessSpecifier, 4 | CXChildVisitResult, 5 | CXCursor, 6 | CXCursorKind, 7 | } from "https://deno.land/x/libclang@1.0.0-beta.8/mod.ts"; 8 | import { Context } from "../Context.ts"; 9 | import { 10 | ClassContent, 11 | ClassEntry, 12 | InlineClassTemplateTypeEntry, 13 | Parameter, 14 | TemplateParameter, 15 | TypeEntry, 16 | } from "../types.d.ts"; 17 | import { getFileNameFromCursor, getNamespacedName } from "../utils.ts"; 18 | import { getClassSpecializationByCursor } from "./ClassTemplate.ts"; 19 | import { visitFunctionCursor } from "./Function.ts"; 20 | import { visitType } from "./Type.ts"; 21 | 22 | const PLAIN_METHOD_NAME_REGEX = /^[\w_]+$/i; 23 | 24 | export const visitClassCursor = ( 25 | context: Context, 26 | /** 27 | * Must have kind ClassDecl or StructDecl 28 | */ 29 | cursor: CXCursor, 30 | importEntry?: ClassContent, 31 | ): ClassEntry => { 32 | const classEntry = context.findClassByCursor(cursor); 33 | 34 | if (!classEntry) { 35 | throw new Error(`Could not find class entry for '${cursor.getSpelling()}'`); 36 | } 37 | 38 | return visitClassEntry(context, classEntry, importEntry); 39 | }; 40 | 41 | export const visitClassEntry = ( 42 | context: Context, 43 | classEntry: ClassEntry, 44 | importEntry?: ClassContent, 45 | ): ClassEntry => { 46 | const visitBasesAndFields = !classEntry.used; 47 | 48 | if ( 49 | !visitBasesAndFields && (!importEntry || !importEntry.constructors && 50 | !importEntry.destructors && 51 | (!importEntry.methods || 52 | Array.isArray(importEntry.methods) && 53 | importEntry.methods.length === 0)) 54 | ) { 55 | // We're not going to visit fields, add constructors, destructors 56 | // or methods. Thus we do not need to visit children at all. 57 | return classEntry; 58 | } 59 | 60 | classEntry.used = true; 61 | 62 | classEntry.cursor.visitChildren((gc) => { 63 | if ( 64 | gc.kind === CXCursorKind.CXCursor_CXXBaseSpecifier && 65 | visitBasesAndFields 66 | ) { 67 | try { 68 | const { 69 | baseClass, 70 | isVirtualBase, 71 | } = visitBaseClass(context, gc, importEntry); 72 | if (isVirtualBase) { 73 | classEntry.virtualBases.push(baseClass); 74 | } else { 75 | classEntry.bases.push(baseClass); 76 | } 77 | } catch (err) { 78 | const baseError = new Error( 79 | `Failed to visit base class '${gc.getSpelling()}' of class '${classEntry.name}'`, 80 | ); 81 | baseError.cause = err; 82 | throw baseError; 83 | } 84 | } else if (gc.kind === CXCursorKind.CXCursor_CXXMethod) { 85 | try { 86 | visitMethod(context, classEntry, gc, importEntry); 87 | } catch (err) { 88 | const newError = new Error( 89 | `Failed to visit method '${gc.getSpelling()}' of class '${classEntry.name}'`, 90 | ); 91 | newError.cause = err; 92 | throw newError; 93 | } 94 | } else if (gc.kind === CXCursorKind.CXCursor_Constructor) { 95 | try { 96 | visitConstructor(context, classEntry, gc, importEntry); 97 | } catch (err) { 98 | const newError = new Error( 99 | `Failed to visit constructor '${gc.getSpelling()}' of class '${classEntry.name}'`, 100 | ); 101 | newError.cause = err; 102 | throw newError; 103 | } 104 | } else if (gc.kind === CXCursorKind.CXCursor_Destructor) { 105 | try { 106 | visitDestructor(context, classEntry, gc, importEntry); 107 | } catch (err) { 108 | const newError = new Error( 109 | `Failed to visit destructor '${gc.getSpelling()}' of class '${classEntry.name}'`, 110 | ); 111 | newError.cause = err; 112 | throw newError; 113 | } 114 | } else if ( 115 | gc.kind === CXCursorKind.CXCursor_FieldDecl && visitBasesAndFields 116 | ) { 117 | const type = gc.getType(); 118 | if (!type) { 119 | throw new Error( 120 | `Could not get type for class field '${gc.getSpelling()}' of class '${classEntry.name}'`, 121 | ); 122 | } 123 | let field: TypeEntry | null; 124 | try { 125 | field = visitType(context, type); 126 | } catch (err) { 127 | // const access = gc.getCXXAccessSpecifier(); 128 | // if ( 129 | // access === CX_CXXAccessSpecifier.CX_CXXPrivate || 130 | // access === CX_CXXAccessSpecifier.CX_CXXProtected 131 | // ) { 132 | // // Failure to accurately describe a private or protected field is not an issue. 133 | // field = "buffer"; 134 | // } else { 135 | const newError = new Error( 136 | `Failed to visit class field '${gc.getSpelling()}' of class '${classEntry.name}'`, 137 | ); 138 | newError.cause = err; 139 | throw newError; 140 | // } 141 | } 142 | if (field === null) { 143 | throw new Error( 144 | `Found void class field '${gc.getSpelling()}' of class '${classEntry.name}'`, 145 | ); 146 | } 147 | if (typeof field === "object" && "used" in field) { 148 | field.used = true; 149 | } 150 | classEntry.fields.push({ 151 | cursor: gc, 152 | name: gc.getSpelling(), 153 | type: field, 154 | }); 155 | } 156 | return CXChildVisitResult.CXChildVisit_Continue; 157 | }); 158 | 159 | return classEntry; 160 | }; 161 | 162 | const visitConstructor = ( 163 | context: Context, 164 | entry: ClassEntry, 165 | cursor: CXCursor, 166 | importEntry?: ClassContent, 167 | ): void => { 168 | if (!importEntry || importEntry.constructors === false) { 169 | // All constructors are ignored. 170 | return; 171 | } 172 | const access = cursor.getCXXAccessSpecifier(); 173 | if ( 174 | access === CX_CXXAccessSpecifier.CX_CXXPrivate || 175 | access === CX_CXXAccessSpecifier.CX_CXXProtected || 176 | cursor.isFunctionInlined() || 177 | typeof importEntry.constructors === "function" && 178 | !importEntry.constructors(cursor) 179 | ) { 180 | // Do not use private or protected constructors. 181 | return; 182 | } 183 | 184 | const manglings = cursor.getCXXManglings(); 185 | 186 | if ( 187 | entry.constructors.some((cons) => 188 | cons.manglings.every((name, index) => name === manglings[index]) 189 | ) 190 | ) { 191 | // Attempt to re-enter the same constructor, ignore. 192 | return; 193 | } 194 | 195 | // Constructors always take a 0th parameter 196 | // pointing to the ClassBuffer. 197 | entry.usedAsBuffer = true; 198 | 199 | const { parameters } = visitFunctionCursor(context, cursor); 200 | entry.constructors.push({ 201 | parameters, 202 | cursor, 203 | manglings, 204 | }); 205 | }; 206 | 207 | const visitDestructor = ( 208 | context: Context, 209 | entry: ClassEntry, 210 | cursor: CXCursor, 211 | importEntry?: ClassContent, 212 | ): void => { 213 | if ( 214 | !importEntry || importEntry.destructors === false || 215 | entry.destructor !== null 216 | ) { 217 | // Destructors should not be included. 218 | return; 219 | } 220 | const access = cursor.getCXXAccessSpecifier(); 221 | if ( 222 | access === CX_CXXAccessSpecifier.CX_CXXPrivate || 223 | access === CX_CXXAccessSpecifier.CX_CXXProtected || 224 | cursor.isFunctionInlined() 225 | ) { 226 | // Do not use private or protected destructors 227 | return; 228 | } 229 | 230 | visitFunctionCursor(context, cursor); 231 | entry.destructor = { 232 | cursor, 233 | manglings: cursor.getCXXManglings(), 234 | }; 235 | }; 236 | 237 | const visitMethod = ( 238 | context: Context, 239 | entry: ClassEntry, 240 | cursor: CXCursor, 241 | importEntry?: ClassContent, 242 | ): void => { 243 | if (!importEntry || importEntry.methods === false) { 244 | // All methods are ignored. 245 | return; 246 | } 247 | const access = cursor.getCXXAccessSpecifier(); 248 | if ( 249 | access === CX_CXXAccessSpecifier.CX_CXXPrivate || 250 | access === CX_CXXAccessSpecifier.CX_CXXProtected || 251 | cursor.isFunctionInlined() 252 | ) { 253 | // Do not use private or protected methods. 254 | return; 255 | } 256 | 257 | const mangling = cursor.getMangling(); 258 | 259 | if ( 260 | entry.methods.some((method) => method.mangling === mangling) 261 | ) { 262 | // Attempt to re-enter the same method, ignore. 263 | return; 264 | } 265 | 266 | const methodName = cursor.getSpelling(); 267 | if ( 268 | methodName.includes("operator") && 269 | !PLAIN_METHOD_NAME_REGEX.test(methodName) 270 | ) { 271 | // Ignore operators. 272 | return; 273 | } 274 | 275 | if ( 276 | typeof importEntry.methods === "function" && 277 | !importEntry.methods(methodName, cursor) 278 | ) { 279 | // Method filter returned false. 280 | return; 281 | } else if ( 282 | Array.isArray(importEntry.methods) && 283 | !importEntry.methods.some((name) => name === methodName) 284 | ) { 285 | // Not requested in methods array. 286 | return; 287 | } 288 | 289 | const { parameters, result } = visitFunctionCursor(context, cursor); 290 | entry.methods.push({ 291 | parameters, 292 | cursor, 293 | mangling, 294 | name: methodName, 295 | result, 296 | }); 297 | }; 298 | 299 | export const visitBaseClass = ( 300 | context: Context, 301 | /** 302 | * Must have kind CXXBaseSpecifier 303 | */ 304 | cursor: CXCursor, 305 | importEntry?: ClassContent, 306 | ) => { 307 | const definition = cursor.getDefinition(); 308 | if (!definition) { 309 | throw new Error( 310 | `Could not get definition of base class '${cursor.getSpelling()}'`, 311 | ); 312 | } 313 | importEntry = importEntry 314 | ? { 315 | // Constructors are always concrete, inheritance 316 | // doesn't need the parent constructors in API. 317 | constructors: false, 318 | // Destructors might be relevant? 319 | destructors: importEntry.destructors, 320 | kind: "class", 321 | methods: importEntry.methods, 322 | name: definition.getSpelling(), 323 | } 324 | : undefined; 325 | const isVirtualBase = cursor.isVirtualBase(); 326 | const baseClass = context.visitClassLikeByCursor(definition, importEntry); 327 | if (baseClass.kind === "class") { 328 | const parameters: (Parameter | TemplateParameter)[] = []; 329 | const type = cursor.getType()!; 330 | const targc = type.getNumberOfTemplateArguments(); 331 | for (let i = 0; i < targc; i++) { 332 | const targType = type.getTemplateArgumentAsType(i)!; 333 | const kind = targType.kind; 334 | if (kind === CXTypeKind.CXType_Unexposed) { 335 | // Template parameter 336 | const targName = targType.getSpelling(); 337 | parameters.push( 338 | { 339 | kind: "", 340 | name: targName.replace("...", "").replace(" &&", "").replace( 341 | " &", 342 | "", 343 | ), 344 | isSpread: targName.includes("..."), 345 | isRef: targName.includes(" &"), 346 | } satisfies TemplateParameter, 347 | ); 348 | } else if ( 349 | kind === CXTypeKind.CXType_Bool || 350 | kind === CXTypeKind.CXType_Char_U || 351 | kind === CXTypeKind.CXType_UChar || 352 | kind === CXTypeKind.CXType_UShort || 353 | kind === CXTypeKind.CXType_UInt || 354 | kind === CXTypeKind.CXType_ULong || 355 | kind === CXTypeKind.CXType_ULongLong || 356 | kind === CXTypeKind.CXType_Char_S || 357 | kind === CXTypeKind.CXType_SChar || 358 | kind === CXTypeKind.CXType_Short || 359 | kind === CXTypeKind.CXType_Int || 360 | kind === CXTypeKind.CXType_Long || 361 | kind === CXTypeKind.CXType_LongLong || 362 | kind === CXTypeKind.CXType_Float || 363 | kind === CXTypeKind.CXType_Double || 364 | kind === CXTypeKind.CXType_NullPtr 365 | ) { 366 | parameters.push({ 367 | comment: null, 368 | kind: "parameter", 369 | name: targType.getSpelling(), 370 | type: visitType(context, targType)!, 371 | }); 372 | } else { 373 | throw new Error("Missing template argument kind handling"); 374 | } 375 | } 376 | const specialization = getClassSpecializationByCursor( 377 | baseClass, 378 | definition.kind === CXCursorKind.CXCursor_StructDecl 379 | ? definition.getSpecializedTemplate()! 380 | : definition, 381 | ); 382 | if (!specialization) { 383 | throw new Error("Could not find specialization"); 384 | } 385 | return { 386 | baseClass: { 387 | cursor: definition, 388 | file: getFileNameFromCursor(definition), 389 | kind: "inline class", 390 | parameters, 391 | template: baseClass, 392 | specialization, 393 | type, 394 | name: definition.getSpelling(), 395 | nsName: getNamespacedName(definition), 396 | } satisfies InlineClassTemplateTypeEntry, 397 | isVirtualBase, 398 | }; 399 | } else { 400 | return { baseClass, isVirtualBase }; 401 | } 402 | }; 403 | -------------------------------------------------------------------------------- /lib/mod.ts: -------------------------------------------------------------------------------- 1 | import { 2 | basename, 3 | dirname, 4 | relative, 5 | } from "https://deno.land/std@0.170.0/path/mod.ts"; 6 | import upperCase from "https://deno.land/x/case@2.1.1/upperCase.ts"; 7 | import { 8 | CXChildVisitResult, 9 | CXCursorKind, 10 | } from "https://deno.land/x/libclang@1.0.0-beta.8/include/typeDefinitions.ts"; 11 | import { 12 | CXCursor, 13 | CXIndex, 14 | } from "https://deno.land/x/libclang@1.0.0-beta.8/mod.ts"; 15 | import { Context } from "./Context.ts"; 16 | import { 17 | handleImports, 18 | renderFile, 19 | renderSystemFileConstant, 20 | } from "./renderer.ts"; 21 | import { 22 | AbsoluteBindingsFilePath, 23 | AbsoluteClassesFilePath, 24 | AbsoluteFilePath, 25 | AbsoluteSystemBindingsFilePath, 26 | AbsoluteSystemClassesFilePath, 27 | AbsoluteSystemTypesFilePath, 28 | AbsoluteTypesFilePath, 29 | ExportConfiguration, 30 | ImportMap, 31 | RenderData, 32 | RenderDataEntry, 33 | } from "./types.d.ts"; 34 | import { 35 | FFI, 36 | sortRenderDataEntries, 37 | SYSTEM_BINDINGS, 38 | SYSTEM_CLASSES, 39 | SYSTEM_TYPES, 40 | } from "./utils.ts"; 41 | 42 | export const build = (configuration: ExportConfiguration) => { 43 | if ( 44 | configuration.basePath.startsWith(configuration.outputPath) 45 | ) { 46 | throw new Error( 47 | "Base path is inside the output path", 48 | ); 49 | } 50 | const includesFileName = Deno.makeTempFileSync(); 51 | if (new Set(configuration.include).size !== configuration.include.length) { 52 | throw new Error( 53 | "Found duplicate include entries", 54 | ); 55 | } 56 | if (new Set(configuration.files).size !== configuration.files.length) { 57 | throw new Error( 58 | "Found duplicate file entries", 59 | ); 60 | } 61 | Deno.writeTextFileSync( 62 | includesFileName, 63 | configuration.files.map((file) => `#include <${file}>\n`) 64 | .join(""), 65 | ); 66 | 67 | const index = new CXIndex(true, true); 68 | 69 | const includes = configuration.include.map((name) => `-I${name}`); 70 | includes.push("-xc++"); 71 | includes.push("-std=c++20"); 72 | const tu = index.parseTranslationUnit( 73 | includesFileName, 74 | includes, 75 | ); 76 | 77 | Deno.removeSync(includesFileName); 78 | 79 | const context = new Context(); 80 | 81 | const tuCursor = tu.getCursor(); 82 | 83 | gatherEntries(context, tuCursor); 84 | 85 | context.entriesGathered(); 86 | 87 | for (const importEntry of configuration.imports) { 88 | if (importEntry.kind === "class") { 89 | context.visitClass(importEntry); 90 | } else if (importEntry.kind === "function") { 91 | context.visitFunction(importEntry); 92 | } else if (importEntry.kind === "var") { 93 | context.visitVar(importEntry); 94 | } 95 | } 96 | 97 | const usedData = context.getUsedData(); 98 | const ffiExports = new Set(); 99 | const entriesNeededInSystem = new Set(); 100 | const files: RenderData[] = []; 101 | const outsideFiles: RenderData[] = []; 102 | for (const [file, data] of usedData) { 103 | const renderData = renderFile(file, data); 104 | for (const entry of renderData.bindings) { 105 | if (ffiExports.has(entry)) { 106 | throw new Error(`Duplicate export name '${entry}'`); 107 | } 108 | ffiExports.add(entry); 109 | } 110 | handleImports( 111 | configuration.basePath, 112 | entriesNeededInSystem, 113 | renderData.importsInBindingsFile, 114 | ); 115 | handleImports( 116 | configuration.basePath, 117 | entriesNeededInSystem, 118 | renderData.importsInClassesFile, 119 | ); 120 | handleImports( 121 | configuration.basePath, 122 | entriesNeededInSystem, 123 | renderData.importsInTypesFile, 124 | ); 125 | if (renderData.bindingsFilePath.startsWith(configuration.basePath)) { 126 | files.push(renderData); 127 | } else { 128 | outsideFiles.push(renderData); 129 | } 130 | } 131 | 132 | const systemBindingsFilePath = 133 | `${configuration.basePath}/systemBindings.ts` as const; 134 | const systemClassesFilePath = 135 | `${configuration.basePath}/systemClasses.ts` as const; 136 | const systemTypesFilePath = 137 | `${configuration.basePath}/systemTypes.ts` as const; 138 | const entriesInSystemBindingsFile: RenderDataEntry[] = []; 139 | const entriesInSystemClassesFile: RenderDataEntry[] = []; 140 | const entriesInSystemTypesFile: RenderDataEntry[] = []; 141 | const importsInSystemBindingsFile: ImportMap = new Map(); 142 | const importsInSystemClassesFile: ImportMap = new Map(); 143 | const importsInSystemTypesFile: ImportMap = new Map(); 144 | 145 | for (const entry of entriesNeededInSystem) { 146 | const result = renderSystemFileConstant(entry); 147 | if ( 148 | result && 149 | !entriesInSystemTypesFile.some((entry) => 150 | entry.contents === result.contents 151 | ) 152 | ) { 153 | entriesInSystemTypesFile.push(result); 154 | } 155 | } 156 | 157 | entriesInSystemTypesFile.sort(); 158 | 159 | for (const outsideFile of outsideFiles) { 160 | entriesInSystemBindingsFile.push(...outsideFile.entriesInBindingsFile); 161 | entriesInSystemClassesFile.push(...outsideFile.entriesInClassesFile); 162 | entriesInSystemTypesFile.push(...outsideFile.entriesInTypesFile); 163 | for (const [key, value] of outsideFile.importsInBindingsFile) { 164 | importsInSystemBindingsFile.set(key, value); 165 | } 166 | for (const [key, value] of outsideFile.importsInClassesFile) { 167 | importsInSystemClassesFile.set(key, value); 168 | } 169 | for (const [key, value] of outsideFile.importsInTypesFile) { 170 | importsInSystemTypesFile.set(key, value); 171 | } 172 | } 173 | 174 | sortRenderDataEntries(entriesInSystemClassesFile); 175 | sortRenderDataEntries(entriesInSystemTypesFile); 176 | 177 | files.unshift({ 178 | bindings: new Set(), 179 | bindingsFilePath: systemBindingsFilePath, 180 | classesFilePath: systemClassesFilePath, 181 | entriesInBindingsFile: entriesInSystemBindingsFile, 182 | entriesInClassesFile: entriesInSystemClassesFile, 183 | entriesInTypesFile: entriesInSystemTypesFile, 184 | importsInBindingsFile: importsInSystemBindingsFile, 185 | importsInClassesFile: importsInSystemClassesFile, 186 | importsInTypesFile: importsInSystemTypesFile, 187 | typesFilePath: systemTypesFilePath, 188 | }); 189 | 190 | const pathData = { 191 | basePath: configuration.basePath, 192 | outputPath: configuration.outputPath, 193 | systemBindingsFilePath, 194 | systemClassesFilePath, 195 | systemTypesFilePath, 196 | }; 197 | const bindingsImports: string[] = []; 198 | for (const file of files) { 199 | if (file.entriesInBindingsFile.length) { 200 | bindingsImports.push(file.bindingsFilePath); 201 | } 202 | writeFilesData(pathData, file); 203 | } 204 | 205 | const ffiFilePath = `${configuration.basePath}/ffi.ts`; 206 | Deno.writeTextFileSync( 207 | `${configuration.outputPath}/ffi.ts`, 208 | `${ 209 | bindingsImports.map((filePath) => 210 | `import * as ${ 211 | upperCase(basename(filePath).replace(".h.ts", "")) 212 | } from "${makeRelative(relative(ffiFilePath, filePath)).substring(1)}";` 213 | ).join("\n") 214 | } 215 | 216 | const lib = Deno.dlopen("FFIPATH", { 217 | ${ 218 | bindingsImports.map((filePath) => 219 | `...${upperCase(basename(filePath).replace(".h.ts", ""))}` 220 | ) 221 | .join(",\n ") 222 | } 223 | });; 224 | 225 | ${ 226 | [...ffiExports].map((name) => 227 | `export const ${name} = lib.symbols.${name};` 228 | ).join("\n") 229 | }`, 230 | ); 231 | 232 | new Deno.Command("deno", { args: ["fmt", configuration.outputPath] }) 233 | .outputSync(); 234 | }; 235 | 236 | const gatherEntries = (context: Context, parentCursor: CXCursor) => 237 | void parentCursor.visitChildren((cursor) => { 238 | switch (cursor.kind) { 239 | case CXCursorKind.CXCursor_ClassTemplate: 240 | context.addClassTemplate(cursor); 241 | break; 242 | case CXCursorKind.CXCursor_ClassTemplatePartialSpecialization: 243 | context.addClassTemplatePartialSpecialization(cursor); 244 | break; 245 | case CXCursorKind.CXCursor_FunctionDecl: 246 | context.addFunction(cursor); 247 | break; 248 | case CXCursorKind.CXCursor_FunctionTemplate: 249 | /** 250 | * TODO: Support templates 251 | */ 252 | break; 253 | case CXCursorKind.CXCursor_TypeAliasTemplateDecl: 254 | /** 255 | * TODO: Very advanced stuff 256 | * 257 | * @example 258 | * ```c++ 259 | * template 260 | * using __ummap_traits = __detail::_Hashtable_traits<_Cache, false, false>; 261 | * ``` 262 | */ 263 | break; 264 | case CXCursorKind.CXCursor_UnionDecl: 265 | context.addUnion(cursor); 266 | break; 267 | case CXCursorKind.CXCursor_VarDecl: 268 | context.addVar(cursor); 269 | break; 270 | case CXCursorKind.CXCursor_Namespace: 271 | // Recurse into namespaces. 272 | context.pushToNamespaceStack(cursor.getSpelling()); 273 | gatherEntries(context, cursor); 274 | context.popFromNamespaceStack(); 275 | break; 276 | case CXCursorKind.CXCursor_ClassDecl: 277 | case CXCursorKind.CXCursor_StructDecl: 278 | if (!cursor.isDefinition()) { 279 | // Forward-declarations are meaningless 280 | break; 281 | } 282 | 283 | // Recurse into classes first and add all entries 284 | // defined within our class definition. 285 | context.pushToNamespaceStack(cursor.getSpelling()); 286 | gatherEntries(context, cursor); 287 | context.popFromNamespaceStack(); 288 | 289 | // Add the class only after 290 | context.addClass(cursor); 291 | break; 292 | case CXCursorKind.CXCursor_TypedefDecl: 293 | case CXCursorKind.CXCursor_TypeAliasDecl: 294 | context.addTypeDefinition(cursor); 295 | break; 296 | case CXCursorKind.CXCursor_EnumDecl: 297 | context.addEnum(cursor); 298 | break; 299 | case CXCursorKind.CXCursor_UnexposedDecl: 300 | // UnexposedDecl can be `extern "C"`, which we want to look 301 | // inside of. We can just recurse into it as it doesn't 302 | // move namespaces or anything. 303 | return CXChildVisitResult.CXChildVisit_Recurse; 304 | } 305 | return CXChildVisitResult.CXChildVisit_Continue; 306 | }); 307 | 308 | const writeFilesData = ( 309 | pathData: { 310 | basePath: AbsoluteFilePath; 311 | outputPath: AbsoluteFilePath; 312 | systemBindingsFilePath: 313 | | AbsoluteBindingsFilePath 314 | | AbsoluteSystemBindingsFilePath; 315 | systemClassesFilePath: 316 | | AbsoluteClassesFilePath 317 | | AbsoluteSystemClassesFilePath; 318 | systemTypesFilePath: AbsoluteTypesFilePath | AbsoluteSystemTypesFilePath; 319 | }, 320 | { 321 | bindingsFilePath, 322 | classesFilePath, 323 | entriesInBindingsFile, 324 | entriesInClassesFile, 325 | entriesInTypesFile, 326 | importsInBindingsFile, 327 | importsInClassesFile, 328 | importsInTypesFile, 329 | typesFilePath, 330 | }: RenderData, 331 | ) => { 332 | const { 333 | basePath, 334 | } = pathData; 335 | if ( 336 | !bindingsFilePath.startsWith(basePath) || 337 | !classesFilePath.startsWith(basePath) || !typesFilePath.startsWith(basePath) 338 | ) { 339 | throw new Error("Unexpected"); 340 | } 341 | writeFileData( 342 | pathData, 343 | bindingsFilePath, 344 | importsInBindingsFile, 345 | entriesInBindingsFile, 346 | ); 347 | writeFileData( 348 | pathData, 349 | classesFilePath, 350 | importsInClassesFile, 351 | entriesInClassesFile, 352 | ); 353 | writeFileData( 354 | pathData, 355 | typesFilePath, 356 | importsInTypesFile, 357 | entriesInTypesFile, 358 | ); 359 | }; 360 | 361 | const collator = new Intl.Collator("en"); 362 | 363 | const compareStrings = (a: string, b: string) => { 364 | if (a.startsWith("type ")) { 365 | a = a.substring(5); 366 | } 367 | if (b.startsWith("type ")) { 368 | b = b.substring(5); 369 | } 370 | return collator.compare(a, b); 371 | }; 372 | 373 | const writeFileData = ( 374 | { 375 | basePath, 376 | outputPath, 377 | systemBindingsFilePath, 378 | systemClassesFilePath, 379 | systemTypesFilePath, 380 | }: { 381 | basePath: AbsoluteFilePath; 382 | outputPath: AbsoluteFilePath; 383 | systemBindingsFilePath: 384 | | AbsoluteBindingsFilePath 385 | | AbsoluteSystemBindingsFilePath; 386 | systemClassesFilePath: 387 | | AbsoluteClassesFilePath 388 | | AbsoluteSystemClassesFilePath; 389 | systemTypesFilePath: AbsoluteTypesFilePath | AbsoluteSystemTypesFilePath; 390 | }, 391 | filePath: AbsoluteFilePath, 392 | importsInFile: ImportMap, 393 | entriesInFile: RenderDataEntry[], 394 | ) => { 395 | if (!entriesInFile.length) { 396 | return; 397 | } 398 | const importsInFileByFile = new Map(); 399 | for (let [importString, importPath] of importsInFile) { 400 | if (importPath === SYSTEM_BINDINGS) { 401 | importPath = systemBindingsFilePath; 402 | } else if (importPath === SYSTEM_CLASSES) { 403 | importPath = systemClassesFilePath; 404 | } else if (importPath === SYSTEM_TYPES) { 405 | importPath = systemTypesFilePath; 406 | } else if (importPath === FFI) { 407 | importPath = `${basePath}/ffi.ts` as const; 408 | } 409 | if (importPath === filePath) { 410 | continue; 411 | } 412 | const importsList = importsInFileByFile.get(importPath) || 413 | importsInFileByFile.set(importPath, []).get(importPath)!; 414 | importsList.push(importString); 415 | } 416 | 417 | const importEntries: string[] = []; 418 | for ( 419 | const importPath of [...importsInFileByFile.keys()].sort(compareStrings) 420 | ) { 421 | const importsList = importsInFileByFile.get(importPath)!; 422 | importsList.sort(compareStrings); 423 | importEntries.push( 424 | `import { ${importsList.join(", ")} } from "${ 425 | makeRelative(relative(dirname(filePath), importPath)) 426 | }";`, 427 | ); 428 | } 429 | 430 | if (importEntries.length) { 431 | importEntries.push(""); 432 | } 433 | 434 | const fileContents = [ 435 | ...importEntries, 436 | ...entriesInFile.map((entry) => entry.contents), 437 | ].join("\n"); 438 | 439 | const outputFileName = filePath.replace(basePath, outputPath); 440 | 441 | Deno.mkdirSync(dirname(outputFileName), { recursive: true }); 442 | Deno.writeTextFileSync(outputFileName, fileContents); 443 | }; 444 | 445 | const makeRelative = (path: string) => 446 | !path.startsWith(".") ? `./${path}` : path; 447 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CXChildVisitResult, 3 | CXCursorKind, 4 | } from "https://deno.land/x/libclang@1.0.0-beta.8/include/typeDefinitions.ts"; 5 | import { 6 | CXCursor, 7 | CXType, 8 | CXTypeKind, 9 | } from "https://deno.land/x/libclang@1.0.0-beta.8/mod.ts"; 10 | import { SEP } from "./Context.ts"; 11 | import type { 12 | AbsoluteBindingsFilePath, 13 | AbsoluteClassesFilePath, 14 | AbsoluteFilePath, 15 | AbsoluteTypesFilePath, 16 | ClassEntry, 17 | ConstantArrayTypeEntry, 18 | FunctionEntry, 19 | FunctionTypeEntry, 20 | InlineClassTemplateTypeEntry, 21 | InlineClassTypeEntry, 22 | InlineUnionTypeEntry, 23 | MemberPointerTypeEntry, 24 | PlainTypeString, 25 | PointerTypeEntry, 26 | RenderDataEntry, 27 | TypedefEntry, 28 | TypeEntry, 29 | UnionEntry, 30 | } from "./types.d.ts"; 31 | 32 | export const SYSTEM_BINDINGS = "#SYSTEM_B" as const; 33 | export const SYSTEM_CLASSES = "#SYSTEM_C" as const; 34 | export const SYSTEM_TYPES = "#SYSTEM_T" as const; 35 | export const FFI = "#FFI" as const; 36 | 37 | export const getFileNameFromCursor = (cursor: CXCursor): AbsoluteFilePath => 38 | cursor.getLocation().getFileLocation().file.getName() as AbsoluteFilePath; 39 | export const getCursorFileLocation = (cursor: CXCursor): string => { 40 | const fileLocation = cursor.getLocation().getFileLocation(); 41 | return `${fileLocation.file.getName()}:${fileLocation.line}:${fileLocation.column}`; 42 | }; 43 | 44 | export const getCursorNameTemplatePart = (cursor: CXCursor) => { 45 | if (!cursor.getSpecializedTemplate()) { 46 | return ""; 47 | } 48 | 49 | let nameTemplatePart = ""; 50 | const type = cursor.getType(); 51 | if (!type) { 52 | throw new Error("Should find a type"); 53 | } 54 | nameTemplatePart = "<"; 55 | const targc = type.getNumberOfTemplateArguments(); 56 | for (let i = 0; i < targc; i++) { 57 | if (i > 0) { 58 | nameTemplatePart += ", "; 59 | } 60 | const targ = type.getTemplateArgumentAsType(i); 61 | if (!targ) { 62 | nameTemplatePart += "unknown"; 63 | } else { 64 | nameTemplatePart += targ.getSpelling(); 65 | } 66 | } 67 | 68 | nameTemplatePart += ">"; 69 | return nameTemplatePart; 70 | }; 71 | 72 | export const getPlainTypeInfo = ( 73 | typekind: CXTypeKind, 74 | type: CXType, 75 | ): null | PlainTypeString => { 76 | if (typekind === CXTypeKind.CXType_Void) { 77 | return null; 78 | } else if (typekind === CXTypeKind.CXType_Bool) { 79 | return "bool"; 80 | } else if (typekind === CXTypeKind.CXType_Float) { 81 | if (type.getSizeOf() !== 4) { 82 | throw new Error( 83 | `Unexpected Float size: Expected 32, got ${type.getSizeOf() * 8}`, 84 | ); 85 | } 86 | return "f32"; 87 | } else if (typekind === CXTypeKind.CXType_Double) { 88 | if (type.getSizeOf() !== 8) { 89 | throw new Error( 90 | `Unexpected Double size: Expected 64, got ${type.getSizeOf() * 8}`, 91 | ); 92 | } 93 | return "f64"; 94 | } else if (typekind === CXTypeKind.CXType_NullPtr) { 95 | return "pointer"; 96 | } else if ( 97 | typekind === CXTypeKind.CXType_Char_U || 98 | typekind === CXTypeKind.CXType_UChar || 99 | typekind === CXTypeKind.CXType_UShort || 100 | typekind === CXTypeKind.CXType_UInt || 101 | typekind === CXTypeKind.CXType_ULong || 102 | typekind === CXTypeKind.CXType_ULongLong 103 | ) { 104 | // Unsigned number, get size. 105 | const size = type.getSizeOf(); 106 | if (size === 1) { 107 | return "u8"; 108 | } else if (size === 2) { 109 | return "u16"; 110 | } else if (size === 4) { 111 | return "u32"; 112 | } else if (size === 8) { 113 | return "u64"; 114 | } else { 115 | throw new Error(`Unexpected ${type.getKindSpelling()} size: Got ${size}`); 116 | } 117 | } else if ( 118 | typekind === CXTypeKind.CXType_Char_S || 119 | typekind === CXTypeKind.CXType_SChar || 120 | typekind === CXTypeKind.CXType_Short || 121 | typekind === CXTypeKind.CXType_Int || 122 | typekind === CXTypeKind.CXType_Long || 123 | typekind === CXTypeKind.CXType_LongLong 124 | ) { 125 | // Signed number, get size. 126 | const size = type.getSizeOf(); 127 | if (size === 1) { 128 | return "i8"; 129 | } else if (size === 2) { 130 | return "i16"; 131 | } else if (size === 4) { 132 | return "i32"; 133 | } else if (size === 8) { 134 | return "i64"; 135 | } else { 136 | throw new Error(`Unexpected ${type.getKindSpelling()} size: Got ${size}`); 137 | } 138 | } else { 139 | throw new Error(`Unexpected type kind: ${type.getKindSpelling()}`); 140 | } 141 | }; 142 | 143 | export const isPassableByValue = ( 144 | entry: "self" | null | TypeEntry, 145 | ): boolean => { 146 | if (typeof entry === "string" || entry === null) { 147 | return true; 148 | } 149 | if ("cursor" in entry) { 150 | if ( 151 | entry.cursor.kind === CXCursorKind.CXCursor_TypedefDecl || 152 | entry.cursor.kind === CXCursorKind.CXCursor_TypeAliasDecl 153 | ) { 154 | return isTypePassableByValue( 155 | entry.cursor.getTypedefDeclarationOfUnderlyingType()!, 156 | ); 157 | } else if (entry.cursor.kind === CXCursorKind.CXCursor_EnumDecl) { 158 | return true; 159 | } 160 | const canonicalType = entry.cursor.getType()!.getCanonicalType(); 161 | const cursor = canonicalType.getTypeDeclaration()!; 162 | if ( 163 | cursor.kind === CXCursorKind.CXCursor_ClassDecl || 164 | cursor.kind === CXCursorKind.CXCursor_StructDecl 165 | ) { 166 | return isClassPassableByValue(cursor); 167 | } 168 | if (!canonicalType) { 169 | return false; 170 | } 171 | return canonicalType.isPODType(); 172 | } else { 173 | if (!("type" in entry)) { 174 | return false; 175 | } 176 | const canonicalType = entry.type.getCanonicalType(); 177 | return isTypePassableByValue(canonicalType); 178 | } 179 | }; 180 | 181 | const isClassPassableByValue = ( 182 | cursor: CXCursor, 183 | ): boolean => { 184 | const result = cursor.visitChildren((child) => { 185 | if (child.kind === CXCursorKind.CXCursor_CXXBaseSpecifier) { 186 | if (!isClassPassableByValue(child.getDefinition()!)) { 187 | return CXChildVisitResult.CXChildVisit_Break; 188 | } 189 | } else if ( 190 | child.kind === CXCursorKind.CXCursor_Constructor && 191 | (child.isCopyConstructor() || child.isMoveConstructor()) || 192 | child.kind === CXCursorKind.CXCursor_Destructor 193 | ) { 194 | // TODO: Should check if all constructors are deleted. Requires libclang 16. 195 | return CXChildVisitResult.CXChildVisit_Break; 196 | } else if ( 197 | child.kind === CXCursorKind.CXCursor_CXXMethod && child.isVirtual() 198 | ) { 199 | return CXChildVisitResult.CXChildVisit_Break; 200 | } 201 | return CXChildVisitResult.CXChildVisit_Continue; 202 | }); 203 | 204 | if (result) { 205 | return false; 206 | } 207 | return true; 208 | }; 209 | 210 | const isTypePassableByValue = ( 211 | type: CXType, 212 | ): boolean => { 213 | if (type.kind === CXTypeKind.CXType_Record) { 214 | return isClassPassableByValue(type.getTypeDeclaration()!); 215 | } else if (type.kind === CXTypeKind.CXType_Typedef) { 216 | const canonicalType = type.getCanonicalType(); 217 | if (!canonicalType.equals(type)) { 218 | return isTypePassableByValue(canonicalType); 219 | } 220 | } else if (type.kind === CXTypeKind.CXType_Elaborated) { 221 | return isTypePassableByValue(type.getNamedType()!); 222 | } else if (type.kind === CXTypeKind.CXType_Unexposed) { 223 | const canonicalType = type.getCanonicalType(); 224 | if (!canonicalType.equals(type)) { 225 | return isTypePassableByValue(canonicalType); 226 | } 227 | } 228 | return true; 229 | }; 230 | 231 | export const getNamespacedName = (cursor: CXCursor): string => { 232 | const name = cursor.getSpelling(); 233 | if (name.includes("<")) { 234 | throw new Error( 235 | "Do not try to get namespaced name of a templated instance", 236 | ); 237 | } 238 | let parent: null | CXCursor = cursor.getSemanticParent(); 239 | if (!parent) { 240 | return name; 241 | } 242 | const namespaceStack: string[] = []; 243 | let previousParent: null | CXCursor = null; 244 | while (parent && (!previousParent || !parent.equals(previousParent))) { 245 | if (parent.isTranslationUnit()) { 246 | break; 247 | } 248 | if ( 249 | parent.kind === CXCursorKind.CXCursor_Namespace || 250 | parent.kind === CXCursorKind.CXCursor_ClassDecl || 251 | parent.kind === CXCursorKind.CXCursor_ClassTemplate 252 | ) { 253 | const parentName = parent.getSpelling(); 254 | if (parentName) { 255 | namespaceStack.unshift(parentName); 256 | } 257 | } 258 | previousParent = parent; 259 | parent = parent.getSemanticParent(); 260 | if (!parent || parent.isTranslationUnit()) { 261 | break; 262 | } 263 | } 264 | return namespaceStack.length > 0 265 | ? `${namespaceStack.join(SEP)}${SEP}${name}` 266 | : name; 267 | }; 268 | 269 | /** 270 | * Create bindings file path from file path 271 | * 272 | * The bindings file only exports FFI symbol descriptions 273 | * for use in `Deno.dlopen()`. 274 | */ 275 | export const bindingsFile = ( 276 | filePath: AbsoluteFilePath, 277 | ): AbsoluteBindingsFilePath => `${filePath}.ts` as const; 278 | /** 279 | * Create classes file path from file path 280 | * 281 | * The classes file contains JavaScript class definitions 282 | * generated from the C++ header bindings and act as the 283 | * intended entry point into the library. 284 | */ 285 | export const classesFile = ( 286 | filePath: AbsoluteFilePath, 287 | ): AbsoluteClassesFilePath => `${filePath}.classes.ts` as const; 288 | /** 289 | * Create types file path from file path 290 | * 291 | * The types file contains FFI symbol type definitions 292 | * and TypeScript type definitions. The FFI symbol type 293 | * definitions are used in the bindings files and in other 294 | * FFI symbol type definitions. The TypeScript type definitions 295 | * are used in class files. 296 | */ 297 | export const typesFile = (filePath: AbsoluteFilePath): AbsoluteTypesFilePath => 298 | `${filePath}.types.ts` as const; 299 | 300 | export const isStructLike = ( 301 | entry: null | "self" | TypeEntry, 302 | ): entry is 303 | | ConstantArrayTypeEntry 304 | | ClassEntry 305 | | InlineClassTypeEntry 306 | | InlineClassTemplateTypeEntry 307 | | InlineUnionTypeEntry 308 | | TypedefEntry => 309 | entry !== null && typeof entry === "object" && 310 | (entry.kind === "[N]" || entry.kind === "class" || 311 | entry.kind === "inline class" || entry.kind === "inline class" || 312 | entry.kind === "inline union" || 313 | entry.kind === "typedef" && isStructLike(entry.target)); 314 | 315 | export const isStruct = ( 316 | entry: null | "self" | TypeEntry, 317 | ): entry is ClassEntry => 318 | entry !== null && typeof entry === "object" && 319 | entry.kind === "class"; 320 | 321 | export const isInlineStruct = ( 322 | entry: null | "self" | TypeEntry, 323 | ): entry is InlineClassTypeEntry => 324 | entry !== null && typeof entry === "object" && 325 | entry.kind === "inline class"; 326 | 327 | export const isInlineTemplateStruct = ( 328 | entry: null | "self" | TypeEntry, 329 | ): entry is InlineClassTemplateTypeEntry => 330 | entry !== null && typeof entry === "object" && 331 | entry.kind === "inline class"; 332 | 333 | export const isConstantArray = ( 334 | entry: null | "self" | TypeEntry, 335 | ): entry is ConstantArrayTypeEntry => 336 | entry !== null && typeof entry === "object" && 337 | entry.kind === "[N]"; 338 | 339 | export const isPointer = ( 340 | entry: null | "self" | TypeEntry, 341 | ): entry is PointerTypeEntry => 342 | entry !== null && typeof entry === "object" && entry.kind === "pointer"; 343 | 344 | export const isFunction = ( 345 | entry: null | "self" | TypeEntry, 346 | ): entry is FunctionEntry | FunctionTypeEntry => 347 | entry !== null && typeof entry === "object" && 348 | (entry.kind === "function" || entry.kind === "fn"); 349 | 350 | export const isPointerToStructLike = ( 351 | entry: null | "self" | TypeEntry, 352 | ): entry is PointerTypeEntry & { 353 | pointee: 354 | | ConstantArrayTypeEntry 355 | | ClassEntry 356 | | InlineClassTypeEntry 357 | | InlineClassTemplateTypeEntry 358 | | InlineUnionTypeEntry 359 | | TypedefEntry; 360 | } => 361 | isPointer(entry) && (entry.pointee === "self" || isStructLike(entry.pointee)); 362 | 363 | export const isStructOrTypedefStruct = ( 364 | entry: null | "self" | TypeEntry, 365 | ): entry is ClassEntry | TypedefEntry => 366 | isStruct(entry) || 367 | isTypedef(entry) && 368 | (isInlineStruct(entry.target) || isStructOrTypedefStruct(entry.target)); 369 | 370 | export const isCharVector = ( 371 | entry: null | "self" | TypeEntry, 372 | ): entry is "cstring" | "cstringArray" | TypedefEntry => 373 | entry === "cstring" || entry === "cstringArray" || 374 | isTypedef(entry) && isCharVector(entry.target); 375 | 376 | export const isTypedef = ( 377 | entry: null | "self" | TypeEntry, 378 | ): entry is TypedefEntry => 379 | entry !== null && typeof entry === "object" && entry.kind === "typedef"; 380 | 381 | export const isUnion = ( 382 | entry: null | "self" | TypeEntry, 383 | ): entry is UnionEntry => 384 | entry !== null && typeof entry === "object" && entry.kind === "union"; 385 | 386 | export const createRenderDataEntry = ( 387 | names: string[] = [], 388 | dependencies: string[] = [], 389 | contents = "", 390 | ): RenderDataEntry => ({ 391 | contents, 392 | dependencies, 393 | names, 394 | }); 395 | 396 | export const createDummyRenderDataEntry = ( 397 | contents: string, 398 | ): RenderDataEntry => ({ 399 | contents, 400 | dependencies: [], 401 | names: [], 402 | }); 403 | 404 | export const sortRenderDataEntries = ( 405 | entries: RenderDataEntry[], 406 | ) => { 407 | for (let i = 0; i < entries.length;) { 408 | const entry = entries[i]; 409 | const firstReferringIndex = entries.findIndex((prevEntry, index) => 410 | index < i && 411 | prevEntry.dependencies.length > 0 && 412 | entry.names.some((name) => prevEntry.dependencies.includes(name)) 413 | ); 414 | 415 | if (firstReferringIndex !== -1) { 416 | // Earlier entry refers to this one: We need to move before it it. 417 | entries.splice(i, 1); 418 | entries.splice(firstReferringIndex, 0, entry); 419 | } else { 420 | // No need to move: Step to next. 421 | i++; 422 | } 423 | } 424 | }; 425 | 426 | /** 427 | * Get size of TypeEntry in bytes 428 | */ 429 | export const getSizeOfType = (entry: null | TypeEntry): number => { 430 | if (entry === null) { 431 | return 1; 432 | } else if (typeof entry === "string") { 433 | switch (entry) { 434 | case "bool": 435 | case "u8": 436 | case "i8": 437 | return 1; 438 | case "u16": 439 | case "i16": 440 | return 2; 441 | case "f32": 442 | case "u32": 443 | case "i32": 444 | return 4; 445 | case "f64": 446 | case "u64": 447 | case "i64": 448 | case "buffer": 449 | case "pointer": 450 | case "cstring": 451 | case "cstringArray": 452 | return 8; 453 | default: 454 | throw new Error("Unimplemented"); 455 | } 456 | } 457 | switch (entry.kind) { 458 | case "fn": 459 | case "function": 460 | case "pointer": 461 | case "member pointer": 462 | return (entry as MemberPointerTypeEntry).type.getSizeOf(); 463 | case "class": 464 | case "class": 465 | case "enum": 466 | case "union": 467 | return entry.cursor.getType()!.getSizeOf(); 468 | case "[N]": 469 | case "inline class": 470 | case "inline class": 471 | return entry.type.getSizeOf(); 472 | case "inline union": 473 | return Math.max( 474 | ...entry.fields.map((field) => getSizeOfType(field.type)), 475 | ); 476 | case "typedef": 477 | return getSizeOfType(entry.target); 478 | default: 479 | throw new Error("Unimplemented"); 480 | } 481 | }; 482 | 483 | export const createSizedStruct = ( 484 | type: CXType, 485 | ): { struct: ("u8" | "u16" | "u32" | "u64")[] } => { 486 | const size = type.getSizeOf(); 487 | const align = type.getAlignOf(); 488 | if (align !== 1 && align !== 2 && align !== 4 && align !== 8) { 489 | throw new Error(`Unexpected union alignment '${align}'`); 490 | } 491 | const unitString = `u${align * 8}` as "u8" | "u16" | "u32" | "u64"; 492 | if (size === align) { 493 | return { struct: [unitString] }; 494 | } 495 | 496 | const count = Math.floor(size / align); 497 | const remainder = size % align; 498 | 499 | if (remainder === 0) { 500 | return { struct: new Array(count).fill(unitString) }; 501 | } else if (!Number.isInteger(remainder)) { 502 | throw new Error(`Unexpected union alignment remainder '${remainder}'`); 503 | } 504 | return { 505 | struct: new Array(count).fill(unitString).concat( 506 | new Array(remainder).fill("u8"), 507 | ), 508 | }; 509 | }; 510 | -------------------------------------------------------------------------------- /lib/visitors/ClassTemplate.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CXChildVisitResult, 3 | CXCursor, 4 | CXCursorKind, 5 | } from "https://deno.land/x/libclang@1.0.0-beta.8/mod.ts"; 6 | import { Context } from "../Context.ts"; 7 | import { 8 | ClassTemplateEntry, 9 | ClassTemplatePartialSpecialization, 10 | InlineClassTemplateTypeEntry, 11 | TemplateParameter, 12 | TypeEntry, 13 | } from "../types.d.ts"; 14 | import { getNamespacedName } from "../utils.ts"; 15 | import { visitBaseClass } from "./Class.ts"; 16 | import { createInlineTypeEntry, visitType } from "./Type.ts"; 17 | 18 | export const visitClassTemplateCursor = ( 19 | context: Context, 20 | cursor: CXCursor, 21 | ): ClassTemplateEntry => { 22 | const classTemplateEntry = context.findClassTemplateByCursor(cursor); 23 | const foundPartialSpecialization = classTemplateEntry && cursor 24 | ? classTemplateEntry.partialSpecializations.find((entry) => 25 | entry.cursor.equals(cursor) 26 | ) 27 | : undefined; 28 | if (!classTemplateEntry) { 29 | throw new Error( 30 | `Could not find class template '${getNamespacedName(cursor)}'`, 31 | ); 32 | } 33 | 34 | return visitClassTemplateEntry( 35 | context, 36 | classTemplateEntry, 37 | foundPartialSpecialization, 38 | ); 39 | }; 40 | 41 | export const getClassSpecializationByCursor = ( 42 | entry: ClassTemplateEntry, 43 | cursor: CXCursor, 44 | ) => { 45 | const specialization = entry.partialSpecializations.find((spec) => 46 | spec.cursor.equals(cursor) 47 | ); 48 | if (specialization) { 49 | return specialization; 50 | } 51 | if (entry.cursor.equals(cursor) && entry.defaultSpecialization) { 52 | return entry.defaultSpecialization; 53 | } else if ( 54 | entry.defaultSpecialization && 55 | entry.defaultSpecialization.cursor.equals(cursor) 56 | ) { 57 | return entry.defaultSpecialization; 58 | } 59 | if (!specialization) { 60 | if ( 61 | entry.cursor.isDefinition() && entry.partialSpecializations.length === 0 62 | ) { 63 | // Only default specialization is available: It must be what we should match. 64 | return entry.defaultSpecialization ?? undefined; 65 | } else if ( 66 | !entry.cursor.isDefinition() && entry.partialSpecializations.length === 1 67 | ) { 68 | // Only one partial specialization is available: We should probably use it. 69 | return entry.partialSpecializations[0]; 70 | } else if ( 71 | !entry.cursor.isDefinition() && entry.partialSpecializations.length === 0 72 | ) { 73 | // No definitions available. Ignore. 74 | return; 75 | } else if (entry.cursor.equals(cursor)) { 76 | // We match the default specialization which doesn't exist. 77 | return entry.defaultSpecialization ?? undefined; 78 | } 79 | throw new Error("Could not find matching specialization"); 80 | } 81 | return specialization; 82 | }; 83 | 84 | export const visitClassTemplateEntry = ( 85 | context: Context, 86 | classTemplateEntry: ClassTemplateEntry, 87 | partialSpecialization?: ClassTemplatePartialSpecialization, 88 | ): ClassTemplateEntry => { 89 | if (!classTemplateEntry.used) { 90 | classTemplateEntry.used = true; 91 | classTemplateEntry.cursor.visitChildren((gc) => { 92 | if ( 93 | gc.kind === CXCursorKind.CXCursor_TemplateTypeParameter 94 | ) { 95 | classTemplateEntry.parameters.push({ 96 | kind: "", 97 | name: gc.getSpelling().replace("...", "").replace(" &&", "") 98 | .replace( 99 | " &", 100 | "", 101 | ), 102 | isSpread: gc.getSpelling().includes("..."), 103 | isRef: gc.getSpelling().includes(" &"), 104 | }); 105 | } else if ( 106 | gc.kind === CXCursorKind.CXCursor_TemplateTemplateParameter 107 | ) { 108 | throw new Error( 109 | `Encountered template template parameter '${gc.getSpelling()} in class template '${classTemplateEntry.nsName}''`, 110 | ); 111 | } 112 | return CXChildVisitResult.CXChildVisit_Continue; 113 | }); 114 | } 115 | 116 | if (!partialSpecialization) { 117 | visitClassTemplateDefaultSpecialization(context, classTemplateEntry); 118 | } else { 119 | visitClassTemplateSpecialization( 120 | context, 121 | classTemplateEntry, 122 | partialSpecialization, 123 | ); 124 | } 125 | 126 | return classTemplateEntry; 127 | }; 128 | 129 | const visitClassTemplateDefaultSpecialization = ( 130 | context: Context, 131 | classTemplateEntry: ClassTemplateEntry, 132 | ) => { 133 | if ( 134 | classTemplateEntry.defaultSpecialization || 135 | !classTemplateEntry.cursor.isDefinition() 136 | ) { 137 | return; 138 | } 139 | 140 | const defaultSpecialization = createDefaultSpecialization( 141 | classTemplateEntry.name, 142 | classTemplateEntry.cursor, 143 | ); 144 | classTemplateEntry.defaultSpecialization = defaultSpecialization; 145 | defaultSpecialization.used = true; 146 | 147 | classTemplateEntry.cursor.visitChildren( 148 | (gc: CXCursor): CXChildVisitResult => { 149 | if ( 150 | gc.kind === CXCursorKind.CXCursor_CXXBaseSpecifier 151 | ) { 152 | try { 153 | const { baseClass, isVirtualBase } = visitBaseClass(context, gc); 154 | if (isVirtualBase) { 155 | defaultSpecialization.virtualBases.push(baseClass); 156 | } else { 157 | defaultSpecialization.bases.push(baseClass); 158 | } 159 | } catch (err) { 160 | const baseError = new Error( 161 | `Failed to visit base class '${gc.getSpelling()}' of class template '${classTemplateEntry.name}' default specialization`, 162 | ); 163 | baseError.cause = err; 164 | throw baseError; 165 | } 166 | } else if (gc.kind === CXCursorKind.CXCursor_CXXMethod) { 167 | // try { 168 | // this.#visitMethod(classEntry, importEntry, gc); 169 | // } catch (err) { 170 | // const newError = new Error( 171 | // `Failed to visit method '${gc.getSpelling()}' of class '${classEntry.name}'`, 172 | // ); 173 | // newError.cause = err; 174 | // throw newError; 175 | // } 176 | } else if (gc.kind === CXCursorKind.CXCursor_Constructor) { 177 | // try { 178 | // this.#visitConstructor(classEntry, importEntry, gc); 179 | // } catch (err) { 180 | // const newError = new Error( 181 | // `Failed to visit constructor '${gc.getSpelling()}' of class '${classEntry.name}'`, 182 | // ); 183 | // newError.cause = err; 184 | // throw newError; 185 | // } 186 | } else if (gc.kind === CXCursorKind.CXCursor_Destructor) { 187 | // try { 188 | // this.#visitDestructor(classEntry, importEntry, gc); 189 | // } catch (err) { 190 | // const newError = new Error( 191 | // `Failed to visit destructor '${gc.getSpelling()}' of class '${classEntry.name}'`, 192 | // ); 193 | // newError.cause = err; 194 | // throw newError; 195 | // } 196 | } else if ( 197 | gc.kind === CXCursorKind.CXCursor_FieldDecl 198 | ) { 199 | const type = gc.getType(); 200 | if (!type) { 201 | throw new Error( 202 | `Could not get type for class field '${gc.getSpelling()}' of class '${classTemplateEntry.name}' defaultSpecialization`, 203 | ); 204 | } 205 | let field: TypeEntry | null; 206 | try { 207 | field = visitType(context, type); 208 | } catch (err) { 209 | // const access = gc.getCXXAccessSpecifier(); 210 | // if ( 211 | // access === CX_CXXAccessSpecifier.CX_CXXPrivate || 212 | // access === CX_CXXAccessSpecifier.CX_CXXProtected 213 | // ) { 214 | // // Failure to accurately describe a private or protected field is not an issue. 215 | // field = "buffer"; 216 | // } else { 217 | const newError = new Error( 218 | `Failed to visit class field '${gc.getSpelling()}' of class '${classTemplateEntry.name}' defaultSpecialization`, 219 | ); 220 | newError.cause = err; 221 | throw newError; 222 | // } 223 | } 224 | if (field === null) { 225 | throw new Error( 226 | `Found void class field '${gc.getSpelling()}' of class '${classTemplateEntry.name}' default specialization`, 227 | ); 228 | } 229 | if (typeof field === "object" && "used" in field) { 230 | field.used = true; 231 | } 232 | defaultSpecialization.fields.push({ 233 | cursor: gc, 234 | name: gc.getSpelling(), 235 | type: field, 236 | }); 237 | } else if ( 238 | gc.kind === CXCursorKind.CXCursor_TemplateTypeParameter 239 | ) { 240 | defaultSpecialization.parameters.push({ 241 | kind: "", 242 | name: gc.getSpelling().replace("...", "").replace(" &&", "") 243 | .replace( 244 | " &", 245 | "", 246 | ), 247 | isSpread: gc.getSpelling().includes("..."), 248 | isRef: gc.getSpelling().includes(" &"), 249 | }); 250 | } else if ( 251 | gc.kind === CXCursorKind.CXCursor_TemplateTemplateParameter 252 | ) { 253 | throw new Error( 254 | `Encountered template template parameter '${gc.getSpelling()} in class template '${classTemplateEntry.nsName}''`, 255 | ); 256 | } 257 | return CXChildVisitResult.CXChildVisit_Continue; 258 | }, 259 | ); 260 | }; 261 | 262 | const visitClassTemplateSpecialization = ( 263 | context: Context, 264 | classTemplateEntry: ClassTemplateEntry, 265 | spec: ClassTemplatePartialSpecialization, 266 | ) => { 267 | if (spec.used) { 268 | return; 269 | } 270 | spec.used = true; 271 | spec.cursor.visitChildren((gc) => { 272 | if ( 273 | gc.kind === CXCursorKind.CXCursor_CXXBaseSpecifier 274 | ) { 275 | try { 276 | const { isVirtualBase, baseClass } = visitBaseClass(context, gc); 277 | if (isVirtualBase) { 278 | spec.virtualBases.push(baseClass); 279 | } else { 280 | spec.bases.push(baseClass); 281 | } 282 | } catch (err) { 283 | const baseError = new Error( 284 | `Failed to visit base class '${gc.getSpelling()}' of class '${classTemplateEntry.name}'`, 285 | ); 286 | baseError.cause = err; 287 | throw baseError; 288 | } 289 | } else if ( 290 | gc.kind === CXCursorKind.CXCursor_FieldDecl 291 | ) { 292 | const type = gc.getType(); 293 | if (!type) { 294 | throw new Error( 295 | `Could not get type for class field '${gc.getSpelling()}' of class '${classTemplateEntry.name}'`, 296 | ); 297 | } 298 | let field: TypeEntry | null; 299 | try { 300 | const found = context.findTypedefByType(type); 301 | if (found) { 302 | field = visitType(context, type); 303 | } else { 304 | field = createInlineTypeEntry(context, type); 305 | } 306 | } catch (err) { 307 | // const access = gc.getCXXAccessSpecifier(); 308 | // if ( 309 | // access === CX_CXXAccessSpecifier.CX_CXXPrivate || 310 | // access === CX_CXXAccessSpecifier.CX_CXXProtected 311 | // ) { 312 | // // Failure to accurately describe a private or protected field is not an issue. 313 | // field = "buffer"; 314 | // } else { 315 | const newError = new Error( 316 | `Failed to visit class field '${gc.getSpelling()}' of class '${classTemplateEntry.name}'`, 317 | ); 318 | newError.cause = err; 319 | throw newError; 320 | // } 321 | } 322 | if (field === null) { 323 | throw new Error( 324 | `Found void class field '${gc.getSpelling()}' of class '${classTemplateEntry.name}'`, 325 | ); 326 | } 327 | if (typeof field === "object" && "used" in field) { 328 | field.used = true; 329 | } 330 | spec.fields.push({ 331 | cursor: gc, 332 | name: gc.getSpelling(), 333 | type: field, 334 | }); 335 | } else if ( 336 | gc.kind === CXCursorKind.CXCursor_TemplateTypeParameter 337 | ) { 338 | spec.parameters.push({ 339 | kind: "", 340 | name: gc.getSpelling(), 341 | isSpread: gc.getPrettyPrinted().includes("typename ..."), 342 | isRef: gc.getPrettyPrinted().includes(" &"), 343 | }); 344 | } else if ( 345 | gc.kind === CXCursorKind.CXCursor_TemplateTemplateParameter 346 | ) { 347 | throw new Error( 348 | `Encountered template template parameter '${gc.getSpelling()} in class template '${classTemplateEntry.nsName}''`, 349 | ); 350 | } 351 | return CXChildVisitResult.CXChildVisit_Continue; 352 | }); 353 | 354 | const specType = spec.cursor.getType()!; 355 | const targc = specType.getNumberOfTemplateArguments(); 356 | for (let i = 0; i < targc; i++) { 357 | const targType = visitType( 358 | context, 359 | specType.getTemplateArgumentAsType(i)!, 360 | ); 361 | spec.application.push(targType!); 362 | } 363 | }; 364 | 365 | export const visitClassTemplateInstance = ( 366 | context: Context, 367 | instance: CXCursor, 368 | ): InlineClassTemplateTypeEntry => { 369 | const ttype = instance.getType(); 370 | const appliedParameters: TemplateParameter[] = []; 371 | if (ttype) { 372 | const ttargc = ttype.getNumberOfTemplateArguments()!; 373 | for (let i = 0; i < ttargc; i++) { 374 | const ttarg = ttype.getTemplateArgumentAsType(i); 375 | if (!ttarg) { 376 | throw new Error("Could not get template argument type"); 377 | } 378 | appliedParameters.push({ 379 | kind: "", 380 | name: ttarg.getSpelling(), 381 | isSpread: ttarg.getTypeDeclaration()?.getPrettyPrinted() 382 | .includes("typename ...") ?? false, 383 | isRef: ttarg.getTypeDeclaration()?.getPrettyPrinted() 384 | .includes(" &") ?? false, 385 | }); 386 | } 387 | } 388 | if ( 389 | instance.kind === 390 | CXCursorKind.CXCursor_ClassTemplatePartialSpecialization 391 | ) { 392 | const specialized = instance.getSpecializedTemplate(); 393 | if (!specialized) { 394 | throw new Error("Unexpected"); 395 | } 396 | const classTemplateEntry = context.findClassTemplateByCursor(specialized); 397 | if (!classTemplateEntry) { 398 | throw new Error("Unexpected"); 399 | } 400 | const found = visitClassTemplateCursor( 401 | context, 402 | instance, 403 | ); 404 | if (ttype === null) { 405 | throw new Error( 406 | `${classTemplateEntry.nsName} instance had a null type value for ${instance.getSpelling()} 407 | `, 408 | ); 409 | } 410 | return { 411 | cursor: found.cursor, 412 | file: found.file, 413 | kind: "inline class", 414 | name: found.name, 415 | nsName: found.nsName, 416 | parameters: appliedParameters, 417 | specialization: getClassSpecializationByCursor(found, instance)!, 418 | template: found, 419 | type: ttype, 420 | }; 421 | } else if ( 422 | instance.kind === CXCursorKind.CXCursor_ClassTemplate 423 | ) { 424 | const found = visitClassTemplateCursor( 425 | context, 426 | instance, 427 | ); 428 | if (ttype === null) { 429 | throw new Error( 430 | `${found.nsName} instance had a null type value for ${instance.getSpelling()} 431 | `, 432 | ); 433 | } 434 | return { 435 | cursor: found.cursor, 436 | file: found.file, 437 | kind: "inline class", 438 | name: found.name, 439 | nsName: found.nsName, 440 | parameters: appliedParameters, 441 | specialization: getClassSpecializationByCursor(found, instance)!, 442 | template: found, 443 | type: ttype, 444 | }; 445 | } else { 446 | throw new Error("Wrooong"); 447 | } 448 | }; 449 | 450 | const createDefaultSpecialization = ( 451 | name: string, 452 | cursor: CXCursor, 453 | ): ClassTemplatePartialSpecialization => ({ 454 | name, 455 | application: [], 456 | bases: [], 457 | constructors: [], 458 | cursor, 459 | destructor: null, 460 | fields: [], 461 | kind: "partial class", 462 | methods: [], 463 | parameters: [], 464 | used: false, 465 | usedAsBuffer: false, 466 | usedAsPointer: false, 467 | virtualBases: [], 468 | }); 469 | 470 | export const renameClassTemplateSpecializations = ( 471 | entry: ClassTemplateEntry, 472 | ) => { 473 | // Don't bother working with partial specializations that are never used. 474 | const usedSpecializations = entry.partialSpecializations.filter((spec) => 475 | spec.used 476 | ); 477 | if (!entry.defaultSpecialization?.used) { 478 | // No default specialization: This template is governed by partial specializations. 479 | if (usedSpecializations.length === 0) { 480 | // A used ClassTemplateEntry should have at least one used specialization somewhere. 481 | throw new Error("Unreachable"); 482 | } else if (usedSpecializations.length === 1) { 483 | // If only one partial specialization is used then it can use the template name directly. 484 | usedSpecializations[0].name = entry.name; 485 | return; 486 | } 487 | } 488 | usedSpecializations.forEach((spec) => { 489 | spec.name = `${entry.name}${spec.parameters.map((x) => x.name).join("")}`; 490 | }); 491 | }; 492 | -------------------------------------------------------------------------------- /lib/visitors/Type.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CXChildVisitResult, 3 | CXCursorKind, 4 | CXType, 5 | CXTypeKind, 6 | } from "https://deno.land/x/libclang@1.0.0-beta.8/mod.ts"; 7 | import { Context } from "../Context.ts"; 8 | import { 9 | ClassField, 10 | ClassTemplateEntry, 11 | ConstantArrayTypeEntry, 12 | FunctionTypeEntry, 13 | InlineClassTemplateTypeEntry, 14 | InlineClassTypeEntry, 15 | Parameter, 16 | PointerTypeEntry, 17 | TemplateParameter, 18 | TypeEntry, 19 | } from "../types.d.ts"; 20 | import { 21 | getCursorFileLocation, 22 | getFileNameFromCursor, 23 | getNamespacedName, 24 | getPlainTypeInfo, 25 | isInlineTemplateStruct, 26 | isPassableByValue, 27 | isPointer, 28 | isStruct, 29 | } from "../utils.ts"; 30 | import { 31 | getClassSpecializationByCursor, 32 | visitClassTemplateCursor, 33 | } from "./ClassTemplate.ts"; 34 | import { visitEnum } from "./Enum.ts"; 35 | import { visitTypedefEntry } from "./Typedef.ts"; 36 | import { visitUnionCursor } from "./Union.ts"; 37 | 38 | export const visitType = (context: Context, type: CXType): null | TypeEntry => { 39 | const kind = type.kind; 40 | if (kind === CXTypeKind.CXType_Void) { 41 | return null; 42 | } 43 | const name = type.isConstQualifiedType() 44 | ? type.getSpelling().substring(6) 45 | : type.getSpelling(); 46 | if (kind === CXTypeKind.CXType_Typedef) { 47 | const declaration = type.getTypeDeclaration(); 48 | const canonicalType = type.getCanonicalType(); 49 | if (!declaration) { 50 | if (type.equals(canonicalType)) { 51 | throw new Error( 52 | "Type equals canonical type and declaration cannot be found", 53 | ); 54 | } 55 | return visitType(context, canonicalType); 56 | } 57 | const found = context.findTypedefByCursor(declaration); 58 | if (found) { 59 | return visitTypedefEntry(context, type.getTypeDeclaration()!); 60 | } 61 | const underlyingType = declaration.getTypedefDeclarationOfUnderlyingType(); 62 | if (!underlyingType) { 63 | if (type.equals(canonicalType)) { 64 | throw new Error( 65 | "Type equals canonical type and underlying type cannot be found", 66 | ); 67 | } 68 | return visitType(context, canonicalType); 69 | } 70 | return visitType(context, underlyingType); 71 | } else if (kind === CXTypeKind.CXType_Unexposed) { 72 | return visitUnexposedType(context, type); 73 | } else if (kind === CXTypeKind.CXType_Elaborated) { 74 | return visitElaboratedType(context, type); 75 | } else if ( 76 | kind === CXTypeKind.CXType_Pointer || 77 | kind === CXTypeKind.CXType_LValueReference || 78 | kind === CXTypeKind.CXType_RValueReference 79 | ) { 80 | const pointee = type.getPointeeType(); 81 | if (!pointee) throw new Error('internal error "pointee" is null'); 82 | if ( 83 | pointee.kind === CXTypeKind.CXType_Char_S 84 | ) { 85 | return "cstring"; 86 | } 87 | const result = visitType(context, pointee); 88 | if ( 89 | pointee.kind === CXTypeKind.CXType_Pointer && 90 | result === "cstring" 91 | ) { 92 | return "cstringArray"; 93 | } else if (result === null) { 94 | return "pointer"; 95 | } 96 | return { 97 | kind: "pointer", 98 | pointee: result, 99 | type, 100 | } satisfies PointerTypeEntry; 101 | } else if ( 102 | kind === CXTypeKind.CXType_Enum 103 | ) { 104 | return visitEnum(context, name); 105 | } else if ( 106 | kind === CXTypeKind.CXType_Bool || 107 | kind === CXTypeKind.CXType_Char_U || 108 | kind === CXTypeKind.CXType_UChar || 109 | kind === CXTypeKind.CXType_UShort || 110 | kind === CXTypeKind.CXType_UInt || 111 | kind === CXTypeKind.CXType_ULong || 112 | kind === CXTypeKind.CXType_ULongLong || 113 | kind === CXTypeKind.CXType_Char_S || 114 | kind === CXTypeKind.CXType_SChar || 115 | kind === CXTypeKind.CXType_Short || 116 | kind === CXTypeKind.CXType_Int || 117 | kind === CXTypeKind.CXType_Long || 118 | kind === CXTypeKind.CXType_LongLong || 119 | kind === CXTypeKind.CXType_Float || 120 | kind === CXTypeKind.CXType_Double || 121 | kind === CXTypeKind.CXType_NullPtr 122 | ) { 123 | if (kind === CXTypeKind.CXType_NullPtr) { 124 | throw new Error(type.getSpelling()); 125 | } 126 | return getPlainTypeInfo(kind, type); 127 | } else if (kind === CXTypeKind.CXType_Record) { 128 | return visitRecordType(context, type); 129 | } else if (kind === CXTypeKind.CXType_IncompleteArray) { 130 | throw new Error("IncompleteArray"); 131 | } else if (kind === CXTypeKind.CXType_ConstantArray) { 132 | const elemType = type.getArrayElementType(); 133 | if (!elemType) { 134 | throw new Error("No ConstantArray element type"); 135 | } 136 | const typeEntry = visitType(context, elemType); 137 | if (typeEntry === null) { 138 | throw new Error("ConstantArray element type is void"); 139 | } 140 | return { 141 | element: typeEntry, 142 | kind: "[N]", 143 | length: type.getArraySize(), 144 | type, 145 | } satisfies ConstantArrayTypeEntry; 146 | } else if (kind === CXTypeKind.CXType_FunctionProto) { 147 | // Function type definitions are presumed to always be callbacks. 148 | // Otherwise they don't make too much sense. 149 | const parameters: Parameter[] = []; 150 | const argc = type.getNumberOfArgumentTypes(); 151 | for (let i = 0; i < argc; i++) { 152 | const argType = type.getArgumentType(i); 153 | if (!argType) { 154 | throw new Error("No arg type for index"); 155 | } 156 | const parameterType = visitType(context, argType); 157 | if (!parameterType) { 158 | throw new Error("Failed to visit parameter type"); 159 | } 160 | 161 | if (isStruct(parameterType)) { 162 | // Struct type as a callback parameter: This is either pass-by-value or pass-by-ref. 163 | // If it is pass-by-value then it comes to Deno as a Uint8Array from which it 164 | // can be transformed into ClassBuffer without allocating through 165 | // `new ClassBuffer(param.buffer)`. 166 | // If it is pass-by-ref then it comes to Deno as a `Deno.PointerObject`. 167 | if (isPassableByValue(parameterType)) { 168 | parameterType.usedAsBuffer = true; 169 | } else { 170 | parameterType.usedAsPointer = true; 171 | } 172 | } else if ( 173 | isInlineTemplateStruct(parameterType) 174 | ) { 175 | // Same goes for template instances. 176 | if (!parameterType.specialization) { 177 | parameterType.specialization = parameterType.template 178 | .defaultSpecialization!; 179 | } 180 | if ( 181 | isPassableByValue(parameterType) 182 | ) { 183 | parameterType.specialization.usedAsBuffer = true; 184 | } else { 185 | parameterType.specialization.usedAsPointer = true; 186 | } 187 | } else if (isPointer(parameterType)) { 188 | // Pointers to struct types come to Deno as `Deno.PointerObject`. 189 | if (isStruct(parameterType.pointee)) { 190 | parameterType.pointee.usedAsPointer = true; 191 | } else if (isInlineTemplateStruct(parameterType.pointee)) { 192 | if (!parameterType.pointee.specialization) { 193 | parameterType.pointee.specialization = parameterType.pointee 194 | .template.defaultSpecialization!; 195 | } 196 | parameterType.pointee.specialization.usedAsPointer = true; 197 | } 198 | } 199 | 200 | parameters.push({ 201 | kind: "parameter", 202 | comment: null, 203 | name: `arg_${i}`, 204 | type: parameterType, 205 | }); 206 | } 207 | const resultType = type.getResultType(); 208 | if (!resultType) { 209 | throw new Error("Failed to get result type"); 210 | } 211 | const result = visitType(context, resultType); 212 | 213 | if (isStruct(result)) { 214 | // By-value struct returns as a Uint8Array, 215 | // by-ref struct takes an extra Uint8Array parameter. 216 | // Either way, the struct ends up as a buffer. 217 | result.usedAsBuffer = true; 218 | } else if (isInlineTemplateStruct(result)) { 219 | // Same thing as above: One way or another the template 220 | // instance struct ends up as a buffer. 221 | if (!result.specialization) { 222 | result.specialization = result.template.defaultSpecialization!; 223 | } 224 | result.specialization.usedAsBuffer = true; 225 | } else if (isPointer(result)) { 226 | if (isStruct(result.pointee)) { 227 | result.pointee.usedAsPointer = true; 228 | } else if (isInlineTemplateStruct(result.pointee)) { 229 | if (!result.pointee.specialization) { 230 | result.pointee.specialization = result.pointee.template 231 | .defaultSpecialization!; 232 | } 233 | result.pointee.specialization.usedAsPointer = true; 234 | } 235 | } 236 | 237 | return { 238 | kind: "fn", 239 | parameters, 240 | result, 241 | type, 242 | } satisfies FunctionTypeEntry; 243 | } else if (kind === CXTypeKind.CXType_MemberPointer) { 244 | return { 245 | type, 246 | kind: "member pointer", 247 | }; 248 | } 249 | throw new Error(`${type.getSpelling()}: ${type.getKindSpelling()}`); 250 | }; 251 | 252 | export const createInlineTypeEntry = ( 253 | context: Context, 254 | type: CXType, 255 | ): TypeEntry => { 256 | if (type.kind === CXTypeKind.CXType_Typedef) { 257 | return createInlineTypeEntry(context, type.getCanonicalType()); 258 | } 259 | if (type.kind !== CXTypeKind.CXType_Record) { 260 | const result = visitType(context, type); 261 | if (result === null) { 262 | throw new Error("Failed to create type"); 263 | } 264 | return result; 265 | } 266 | // Drop out the template part from our inline defined template specification. 267 | if (type.getNumberOfTemplateArguments() <= 0) { 268 | const structCursor = type.getTypeDeclaration(); 269 | if (!structCursor) { 270 | throw new Error("Could not get CXCursor of inline struct"); 271 | } 272 | const isUnion = structCursor.kind === CXCursorKind.CXCursor_UnionDecl; 273 | const fields: ClassField[] = []; 274 | structCursor.visitChildren((maybeFieldCursor) => { 275 | if (maybeFieldCursor.kind === CXCursorKind.CXCursor_FieldDecl) { 276 | const fieldType = visitType(context, maybeFieldCursor.getType()!); 277 | if (!fieldType) { 278 | throw new Error("Field type was void"); 279 | } 280 | fields.push({ 281 | cursor: maybeFieldCursor, 282 | name: maybeFieldCursor.getSpelling(), 283 | type: fieldType, 284 | }); 285 | } 286 | return CXChildVisitResult.CXChildVisit_Continue; 287 | }); 288 | return { 289 | base: null, 290 | fields, 291 | kind: isUnion ? "inline union" : "inline class", 292 | type, 293 | }; 294 | } 295 | const templateCursor = type.getTypeDeclaration(); 296 | if (templateCursor === null) { 297 | throw new Error("Could not find specialized template declaration cursor"); 298 | } 299 | const template = visitClassTemplateCursor( 300 | context, 301 | templateCursor, 302 | ); 303 | const targc = type.getNumberOfTemplateArguments(); 304 | const parameters: Parameter[] = []; 305 | for (let i = 0; i < targc; i++) { 306 | const targType = type.getTemplateArgumentAsType(i); 307 | if (!targType) { 308 | throw new Error("Could not get template argument type"); 309 | } 310 | const parameterType = visitType(context, targType); 311 | if (!parameterType) { 312 | throw new Error("void parameter type"); 313 | } 314 | parameters.push({ 315 | kind: "parameter", 316 | comment: null, 317 | name: "", 318 | type: parameterType, 319 | }); 320 | } 321 | return { 322 | cursor: template.cursor, 323 | parameters, 324 | template, 325 | specialization: getClassSpecializationByCursor(template, templateCursor)!, 326 | kind: "inline class", 327 | name: template.name, 328 | nsName: template.nsName, 329 | type, 330 | file: template.file, 331 | }; 332 | }; 333 | 334 | const visitRecordType = ( 335 | context: Context, 336 | type: CXType, 337 | ) => { 338 | const size = type.getSizeOf(); 339 | const targc = type.getNumberOfTemplateArguments(); 340 | if ( 341 | size === -2 && targc === -1 342 | ) { 343 | // This class or struct is only forward-declared in our headers: 344 | // This is usually not really an issue and we shouldn't care about it. 345 | // It's just an opaque type. If this type needs to be used then we have 346 | // an issue, but most likely this is just used as an opaque pointer in 347 | // which case there is no issue. 348 | return { 349 | base: null, 350 | fields: [], 351 | type, 352 | kind: "inline class", 353 | } satisfies InlineClassTypeEntry; 354 | } 355 | 356 | const declaration = type.getTypeDeclaration(); 357 | 358 | if (!declaration) { 359 | throw new Error(`No type declaration ${type.getSpelling()}`); 360 | } 361 | 362 | if (targc === 0) { 363 | throw new Error("ASD"); 364 | } 365 | 366 | const parameters: Parameter[] = []; 367 | for (let i = 0; i < targc; i++) { 368 | const targType = visitType(context, type.getTemplateArgumentAsType(i)!); 369 | if (targType === null) { 370 | throw new Error("ASD"); 371 | } 372 | parameters.push({ 373 | comment: null, 374 | kind: "parameter", 375 | name: `targ_${i}`, 376 | type: targType, 377 | }); 378 | } 379 | 380 | if (!declaration.getSpelling()) { 381 | // Anonymous declarations are not saved. 382 | return createInlineTypeEntry(context, type); 383 | } 384 | 385 | if ( 386 | declaration.kind === CXCursorKind.CXCursor_ClassDecl || 387 | declaration.kind === CXCursorKind.CXCursor_StructDecl 388 | ) { 389 | const result = context.visitClassLikeByCursor(declaration); 390 | if (result.kind === "class") { 391 | return result; 392 | } else if (result.kind === "typedef") { 393 | return result; 394 | } else if (result.kind === "class") { 395 | return { 396 | cursor: declaration, 397 | file: getFileNameFromCursor(declaration), 398 | kind: "inline class", 399 | parameters, 400 | template: result, 401 | specialization: getClassSpecializationByCursor( 402 | result, 403 | declaration.getSpecializedTemplate()!, 404 | ) || null, 405 | type, 406 | name: declaration.getSpelling(), 407 | nsName: getNamespacedName(declaration), 408 | } satisfies InlineClassTemplateTypeEntry; 409 | } else { 410 | throw new Error("Unexpected result from visitClassLikeByCursor"); 411 | } 412 | } else if (declaration.kind === CXCursorKind.CXCursor_TypedefDecl) { 413 | throw new Error(`Unexpected TypedefDecl '${type.getSpelling()}'`); 414 | // const typedefEntry = context.findTypedefByCursor(declaration); 415 | } else if ( 416 | declaration.kind === CXCursorKind.CXCursor_ClassTemplate || 417 | declaration.kind === 418 | CXCursorKind.CXCursor_ClassTemplatePartialSpecialization 419 | ) { 420 | return visitClassTemplateCursor(context, declaration); 421 | } else if (declaration.kind === CXCursorKind.CXCursor_UnionDecl) { 422 | return visitUnionCursor(context, declaration); 423 | } 424 | 425 | throw new Error(declaration.getKindSpelling()); 426 | // return { 427 | // base: declaration.getSpecializedTemplate() 428 | // ? visitBaseClass( 429 | // context, 430 | // declaration.getSpecializedTemplate()!, 431 | // ).baseClass 432 | // : null, 433 | // fields: [], 434 | // kind: "inline class", 435 | // type, 436 | // } satisfies InlineClassTypeEntry; 437 | }; 438 | 439 | const visitElaboratedType = ( 440 | context: Context, 441 | /** 442 | * Must by of kind Elaborated 443 | */ 444 | type: CXType, 445 | ) => { 446 | const elaborated = type.getNamedType(); 447 | 448 | if (!elaborated) { 449 | throw new Error( 450 | `Unexpectedly could not get named type of elaborated type '${type.getSpelling()}'`, 451 | ); 452 | } 453 | 454 | if (elaborated.kind !== CXTypeKind.CXType_Unexposed) { 455 | // Elaborated type points to something we can analyze normally, continue with that. 456 | return visitType(context, elaborated); 457 | } 458 | 459 | return visitUnexposedType(context, elaborated); 460 | }; 461 | 462 | const visitUnexposedType = (context: Context, type: CXType) => { 463 | const canonicalType = type.getCanonicalType(); 464 | 465 | if (canonicalType.kind !== CXTypeKind.CXType_Unexposed) { 466 | return visitType(context, canonicalType); 467 | } 468 | 469 | // Unexposed types are often some kind of parameters. 470 | const name = type.isConstQualifiedType() 471 | ? type.getSpelling().substring(6) 472 | : type.getSpelling(); 473 | const targc = type.getNumberOfTemplateArguments(); 474 | 475 | if (targc < 0) { 476 | if (name.startsWith("type-parameter-")) { 477 | const isSpread = name.endsWith("..."); 478 | let mutName = isSpread ? name.substring(0, name.length - 3) : name; 479 | const isRvalueRef = mutName.endsWith(" &&"); 480 | const isLvalueRef = mutName.endsWith(" &"); 481 | if (isRvalueRef) { 482 | mutName = mutName.substring(0, mutName.length - 3); 483 | } else if (isLvalueRef) { 484 | mutName = mutName.substring(0, mutName.length - 2); 485 | } 486 | return { 487 | name: mutName, 488 | kind: "", 489 | isSpread, 490 | isRef: isLvalueRef || isRvalueRef, 491 | } satisfies TemplateParameter; 492 | } 493 | const canonicalName = canonicalType.getSpelling(); 494 | if (canonicalName.startsWith("type-parameter-")) { 495 | const isSpread = canonicalName.endsWith("..."); 496 | let mutName = isSpread 497 | ? canonicalName.substring(0, canonicalName.length - 3) 498 | : canonicalName; 499 | const isRvalueRef = mutName.endsWith(" &&"); 500 | const isLvalueRef = mutName.endsWith(" &"); 501 | if (isRvalueRef) { 502 | mutName = mutName.substring(0, mutName.length - 3); 503 | } else if (isLvalueRef) { 504 | mutName = mutName.substring(0, mutName.length - 2); 505 | } 506 | return { 507 | name: mutName, 508 | kind: "", 509 | isSpread, 510 | isRef: isLvalueRef || isRvalueRef, 511 | } satisfies TemplateParameter; 512 | } 513 | 514 | throw new Error( 515 | "Unexpected: Found unexposed non-template type that was is not a type parameter", 516 | ); 517 | } 518 | 519 | const templateDeclaration = type.getTypeDeclaration(); 520 | if (!templateDeclaration) { 521 | throw new Error( 522 | "Could not find type declaration for elaborated template type", 523 | ); 524 | } 525 | 526 | if (templateDeclaration.kind === CXCursorKind.CXCursor_ClassTemplate) { 527 | const classTemplateEntry = visitClassTemplateCursor( 528 | context, 529 | templateDeclaration, 530 | ); 531 | const partialSpecialization = getClassSpecializationByCursor( 532 | classTemplateEntry, 533 | templateDeclaration, 534 | ); 535 | 536 | if (!partialSpecialization) { 537 | throw new Error("Could not determine partial specialization"); 538 | } 539 | 540 | const parameters: (Parameter | TemplateParameter)[] = []; 541 | 542 | for (let i = 0; i < targc; i++) { 543 | const targ = type.getTemplateArgumentAsType(i); 544 | if (!targ) { 545 | throw new Error( 546 | "Unexpectedly got no template argument for index", 547 | ); 548 | } 549 | const targType = visitType(context, targ); 550 | if (!targType) { 551 | throw new Error("Unexpected null template argument type"); 552 | } else if ( 553 | targType === "buffer" && targ.kind === CXTypeKind.CXType_Unexposed 554 | ) { 555 | parameters.push({ 556 | kind: "", 557 | name: targ.getSpelling(), 558 | isSpread: targ.getTypeDeclaration()?.getPrettyPrinted()?.includes( 559 | "...", 560 | ) ?? false, 561 | isRef: targ.getTypeDeclaration()?.getPrettyPrinted()?.includes( 562 | " &", 563 | ) ?? false, 564 | }); 565 | } else { 566 | parameters.push({ 567 | kind: "parameter", 568 | comment: null, 569 | name: targ.getSpelling(), 570 | type: targType, 571 | }); 572 | } 573 | } 574 | 575 | return { 576 | cursor: partialSpecialization?.cursor || classTemplateEntry.cursor, 577 | file: getFileNameFromCursor( 578 | partialSpecialization?.cursor || classTemplateEntry.cursor, 579 | ), 580 | kind: "inline class", 581 | parameters, 582 | specialization: partialSpecialization, 583 | template: classTemplateEntry, 584 | type, 585 | name: partialSpecialization?.name || classTemplateEntry.name, 586 | nsName: classTemplateEntry.nsName, 587 | } satisfies InlineClassTemplateTypeEntry; 588 | } 589 | throw new Error("ASD"); 590 | }; 591 | -------------------------------------------------------------------------------- /lib/Context.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CXChildVisitResult, 3 | CXCursor, 4 | CXType, 5 | } from "https://deno.land/x/libclang@1.0.0-beta.8/mod.ts"; 6 | import { 7 | AbsoluteFilePath, 8 | ClassContent, 9 | ClassEntry, 10 | ClassField, 11 | ClassTemplateEntry, 12 | ClassTemplatePartialSpecialization, 13 | EnumEntry, 14 | FunctionContent, 15 | FunctionEntry, 16 | TypedefEntry, 17 | TypeEntry, 18 | UnionEntry, 19 | UseableEntry, 20 | VarContent, 21 | VarEntry, 22 | } from "./types.d.ts"; 23 | import { 24 | getCursorFileLocation, 25 | getCursorNameTemplatePart, 26 | getFileNameFromCursor, 27 | getNamespacedName, 28 | isConstantArray, 29 | isFunction, 30 | isInlineStruct, 31 | isInlineTemplateStruct, 32 | isPointer, 33 | isStruct, 34 | isTypedef, 35 | isUnion, 36 | } from "./utils.ts"; 37 | import { visitClassEntry } from "./visitors/Class.ts"; 38 | import { 39 | getClassSpecializationByCursor, 40 | renameClassTemplateSpecializations, 41 | visitClassTemplateEntry, 42 | } from "./visitors/ClassTemplate.ts"; 43 | import { visitFunctionCursor } from "./visitors/Function.ts"; 44 | import { visitTypedefEntry } from "./visitors/Typedef.ts"; 45 | import { visitVarEntry } from "./visitors/Var.ts"; 46 | 47 | export const SEP = "::"; 48 | 49 | export class Context { 50 | #classForwardDeclarations: CXCursor[] = []; 51 | #classTemplateForwardDeclarations: CXCursor[] = []; 52 | #classes: ClassEntry[] = []; 53 | #classTemplates: ClassTemplateEntry[] = []; 54 | #enums: EnumEntry[] = []; 55 | #functions: FunctionEntry[] = []; 56 | #nsStack: string[] = []; 57 | #typedefs: TypedefEntry[] = []; 58 | #typedefTemplates = []; 59 | #unions: UnionEntry[] = []; 60 | #vars: VarEntry[] = []; 61 | #useableEntries: UseableEntry[] = []; 62 | 63 | addClass(cursor: CXCursor): void { 64 | if (!cursor.isDefinition()) { 65 | // Forward declaration 66 | const definition = cursor.getDefinition(); 67 | if (definition && !definition.isNull()) { 68 | // Class definition is found in this translation unit 69 | const classEntry = this.#classes.find((entry) => 70 | entry.cursor.equals(definition) 71 | ); 72 | if (classEntry) { 73 | classEntry.forwardDeclarations.push(cursor); 74 | return; 75 | } 76 | } 77 | this.#classForwardDeclarations.push(cursor); 78 | return; 79 | } 80 | const name = cursor.getSpelling(); 81 | if (!name) { 82 | // Anonymous struct 83 | return; 84 | } 85 | 86 | const nameTemplatePart = getCursorNameTemplatePart(cursor); 87 | const nsName = this.#nsStack.length 88 | ? `${this.#nsStack.join("::")}${SEP}${name}${nameTemplatePart}` 89 | : `${name}${nameTemplatePart}`; 90 | 91 | const forwardDeclarations = this.#classForwardDeclarations.filter( 92 | (declCursor) => { 93 | const definition = declCursor.getDefinition(); 94 | return (definition && !definition.isNull() && 95 | definition.equals(cursor)); 96 | }, 97 | ); 98 | 99 | forwardDeclarations.forEach((declCursor) => { 100 | this.#classForwardDeclarations.splice( 101 | this.#classForwardDeclarations.indexOf(declCursor), 102 | 1, 103 | ); 104 | }); 105 | 106 | const entry = { 107 | bases: [], 108 | constructors: [], 109 | cursor, 110 | destructor: null, 111 | fields: [], 112 | file: getFileNameFromCursor(cursor), 113 | forwardDeclarations, 114 | kind: "class", 115 | methods: [], 116 | name, 117 | nsName, 118 | used: false, 119 | usedAsBuffer: false, 120 | usedAsPointer: false, 121 | virtualBases: [], 122 | size: cursor.getType()?.getSizeOf() ?? -1, 123 | } satisfies ClassEntry; 124 | this.#classes.push(entry); 125 | this.#useableEntries.push(entry); 126 | } 127 | 128 | addClassTemplate(cursor: CXCursor): void { 129 | if (!cursor.isDefinition()) { 130 | // Forward declaration 131 | const definition = cursor.getDefinition(); 132 | if (definition && !definition.isNull()) { 133 | // Class definition is found in this translation unit 134 | const classTemplateEntry = this.#classTemplates.find((entry) => 135 | entry.cursor.equals(definition) 136 | ); 137 | if (classTemplateEntry) { 138 | classTemplateEntry.forwardDeclarations.push(cursor); 139 | return; 140 | } 141 | } 142 | this.#classTemplateForwardDeclarations.push(cursor); 143 | return; 144 | } 145 | const name = cursor.getSpelling(); 146 | if (!name) { 147 | // Anonymous struct 148 | return; 149 | } 150 | 151 | const forwardDeclarations = this.#classTemplateForwardDeclarations.filter( 152 | (declCursor) => { 153 | const definition = declCursor.getDefinition(); 154 | return (definition && !definition.isNull() && 155 | definition.equals(cursor)); 156 | }, 157 | ); 158 | 159 | forwardDeclarations.forEach((declCursor) => { 160 | this.#classTemplateForwardDeclarations.splice( 161 | this.#classTemplateForwardDeclarations.indexOf(declCursor), 162 | 1, 163 | ); 164 | }); 165 | 166 | const nsName = this.#nsStack.length 167 | ? `${this.#nsStack.join("::")}${SEP}${name}` 168 | : name; 169 | const entry = { 170 | cursor, 171 | defaultSpecialization: null, 172 | file: getFileNameFromCursor(cursor), 173 | forwardDeclarations, 174 | kind: "class", 175 | name, 176 | nsName, 177 | parameters: [], 178 | partialSpecializations: [], 179 | used: false, 180 | } satisfies ClassTemplateEntry; 181 | this.#classTemplates.push(entry); 182 | this.#useableEntries.push(entry); 183 | } 184 | 185 | addClassTemplatePartialSpecialization(cursor: CXCursor): void { 186 | const spec = cursor.getSpecializedTemplate(); 187 | if (!spec) { 188 | throw new Error("Couldn't get specialized template cursor"); 189 | } 190 | let source = this.#classTemplates.find((entry) => 191 | entry.cursor.equals(spec) 192 | ); 193 | if (!source) { 194 | const forwardDeclarationIndex = this.#classTemplateForwardDeclarations 195 | .findIndex((declCursor) => declCursor.equals(spec)); 196 | if (forwardDeclarationIndex) { 197 | this.#classTemplateForwardDeclarations.splice( 198 | forwardDeclarationIndex, 199 | 1, 200 | ); 201 | const name = spec.getSpelling(); 202 | const nsName = this.#nsStack.length 203 | ? `${this.#nsStack.join("::")}${SEP}${name}` 204 | : name; 205 | 206 | const forwardDeclarations = this.#classTemplateForwardDeclarations 207 | .filter((declCursor) => { 208 | const definition = declCursor.getDefinition(); 209 | return (definition && !definition.isNull() && 210 | definition.equals(cursor)); 211 | }); 212 | 213 | forwardDeclarations.forEach((declCursor) => { 214 | this.#classTemplateForwardDeclarations.splice( 215 | this.#classTemplateForwardDeclarations.indexOf(declCursor), 216 | 1, 217 | ); 218 | }); 219 | 220 | source = { 221 | cursor: spec, 222 | defaultSpecialization: null, 223 | file: getFileNameFromCursor(spec), 224 | forwardDeclarations, 225 | kind: "class", 226 | name, 227 | nsName, 228 | parameters: [], 229 | partialSpecializations: [], 230 | used: false, 231 | } satisfies ClassTemplateEntry; 232 | this.#classTemplates.push(source); 233 | this.#useableEntries.push(source); 234 | } 235 | } 236 | if (!source) { 237 | throw new Error( 238 | `Could not find class template for ${getNamespacedName(cursor)}`, 239 | ); 240 | } 241 | source.partialSpecializations.push({ 242 | name: `${source.name}_${source.partialSpecializations.length}`, 243 | application: [], 244 | bases: [], 245 | constructors: [], 246 | cursor, 247 | destructor: null, 248 | fields: [], 249 | kind: "partial class", 250 | methods: [], 251 | parameters: [], 252 | used: false, 253 | usedAsBuffer: false, 254 | usedAsPointer: false, 255 | virtualBases: [], 256 | }); 257 | } 258 | 259 | addEnum(cursor: CXCursor): void { 260 | if (!cursor.isDefinition()) { 261 | // Forward declaration 262 | return; 263 | } 264 | const name = cursor.getSpelling(); 265 | if (!name) { 266 | // Anonymous enum 267 | return; 268 | } 269 | const nsName = this.#nsStack.length 270 | ? `${this.#nsStack.join("::")}${SEP}${name}` 271 | : name; 272 | 273 | const entry = { 274 | cursor, 275 | file: getFileNameFromCursor(cursor), 276 | kind: "enum", 277 | name, 278 | nsName, 279 | type: null, 280 | used: false, 281 | } satisfies EnumEntry; 282 | this.#enums.push(entry); 283 | this.#useableEntries.push(entry); 284 | } 285 | 286 | addFunction(cursor: CXCursor): void { 287 | const name = cursor.getSpelling(); 288 | if (!name) { 289 | // Anonymous function? 290 | return; 291 | } 292 | 293 | const nsName = this.#nsStack.length 294 | ? `${this.#nsStack.join("::")}${SEP}${name}` 295 | : name; 296 | const entry = { 297 | parameters: [], 298 | cursor, 299 | file: getFileNameFromCursor(cursor), 300 | kind: "function", 301 | mangling: cursor.getMangling(), 302 | name, 303 | nsName, 304 | result: null, 305 | used: false, 306 | } satisfies FunctionEntry; 307 | this.#functions.push(entry); 308 | this.#useableEntries.push(entry); 309 | } 310 | 311 | addTypeDefinition(cursor: CXCursor): void { 312 | if (!cursor.isDefinition()) { 313 | // Forward declaration 314 | return; 315 | } 316 | const name = cursor.getSpelling(); 317 | if (!name) { 318 | // Anonymous definition, this is likely eg. `typedef enum {} Name` 319 | return; 320 | } 321 | 322 | const nameTemplatePart = getCursorNameTemplatePart(cursor); 323 | 324 | const nsName = this.#nsStack.length 325 | ? `${this.#nsStack.join("::")}${SEP}${name}${nameTemplatePart}` 326 | : `${name}${nameTemplatePart}`; 327 | 328 | const entry = { 329 | cursor, 330 | file: getFileNameFromCursor(cursor), 331 | kind: "typedef", 332 | name, 333 | nsName, 334 | target: null, 335 | used: false, 336 | } satisfies TypedefEntry; 337 | this.#typedefs.push(entry); 338 | this.#useableEntries.push(entry); 339 | } 340 | 341 | addVar(cursor: CXCursor): void { 342 | if (!cursor.isDefinition()) { 343 | return; 344 | } 345 | const name = cursor.getSpelling(); 346 | if (!name) { 347 | return; 348 | } 349 | 350 | const nameTemplatePart = getCursorNameTemplatePart(cursor); 351 | 352 | const nsName = this.#nsStack.length 353 | ? `${this.#nsStack.join("::")}${SEP}${name}${nameTemplatePart}` 354 | : `${name}${nameTemplatePart}`; 355 | 356 | const entry = { 357 | cursor, 358 | file: getFileNameFromCursor(cursor), 359 | kind: "var", 360 | mangling: cursor.getMangling(), 361 | name, 362 | nsName, 363 | type: null, 364 | used: false, 365 | } satisfies VarEntry; 366 | this.#vars.push(entry); 367 | this.#useableEntries.push(entry); 368 | } 369 | 370 | addUnion(cursor: CXCursor): void { 371 | if (!cursor.isDefinition()) { 372 | return; 373 | } 374 | const name = cursor.getSpelling(); 375 | if (!name) { 376 | return; 377 | } 378 | 379 | const nameTemplatePart = getCursorNameTemplatePart(cursor); 380 | 381 | const nsName = this.#nsStack.length 382 | ? `${this.#nsStack.join("::")}${SEP}${name}${nameTemplatePart}` 383 | : `${name}${nameTemplatePart}`; 384 | 385 | const entry = { 386 | cursor, 387 | file: getFileNameFromCursor(cursor), 388 | kind: "union", 389 | name, 390 | nsName, 391 | fields: [], 392 | used: false, 393 | } satisfies UnionEntry; 394 | this.#unions.push(entry); 395 | this.#useableEntries.push(entry); 396 | } 397 | 398 | entriesGathered(): void { 399 | let cursor: undefined | CXCursor; 400 | while ((cursor = this.#classTemplateForwardDeclarations.shift())) { 401 | const name = cursor.getSpelling(); 402 | const nsName = this.#nsStack.length 403 | ? `${this.#nsStack.join("::")}${SEP}${name}` 404 | : name; 405 | 406 | const forwardDeclarations = this.#classTemplateForwardDeclarations.filter( 407 | (declCursor) => { 408 | const definition = declCursor.getDefinition(); 409 | return (definition && !definition.isNull() && 410 | definition.equals(cursor!)); 411 | }, 412 | ); 413 | 414 | forwardDeclarations.forEach((declCursor) => { 415 | this.#classTemplateForwardDeclarations.splice( 416 | this.#classTemplateForwardDeclarations.indexOf(declCursor), 417 | 1, 418 | ); 419 | }); 420 | 421 | const entry = { 422 | cursor, 423 | defaultSpecialization: null, 424 | file: getFileNameFromCursor(cursor), 425 | forwardDeclarations, 426 | kind: "class", 427 | name, 428 | nsName, 429 | parameters: [], 430 | partialSpecializations: [], 431 | used: false, 432 | } satisfies ClassTemplateEntry; 433 | this.#classTemplates.push(entry); 434 | this.#useableEntries.push(entry); 435 | } 436 | } 437 | 438 | visitClass(importEntry: ClassContent): void { 439 | const classEntry = this.findClassByName(importEntry.name); 440 | if (classEntry) { 441 | visitClassEntry(this, classEntry, importEntry); 442 | return; 443 | } 444 | const classTemplateEntry = this.findClassTemplateByName(importEntry.name); 445 | if (classTemplateEntry) { 446 | visitClassTemplateEntry(this, classTemplateEntry); 447 | return; 448 | } 449 | const typedefEntry = this.findTypedefByName(importEntry.name); 450 | if (typedefEntry) { 451 | visitTypedefEntry(this, typedefEntry.cursor); 452 | return; 453 | } 454 | throw new Error(`Could not find class with name '${importEntry.name}'`); 455 | } 456 | 457 | visitClassLikeByCursor( 458 | cursor: CXCursor, 459 | importEntry?: ClassContent, 460 | ): ClassEntry | ClassTemplateEntry | TypedefEntry { 461 | if (!cursor.isDefinition()) { 462 | const definition = cursor.getDefinition(); 463 | if (definition && !definition.isNull()) { 464 | cursor = definition; 465 | } 466 | } 467 | const classEntry = this.findClassByCursor(cursor); 468 | if (classEntry) { 469 | return visitClassEntry(this, classEntry, importEntry); 470 | } 471 | const classTemplateEntry = this.findClassTemplateByCursor(cursor); 472 | if (classTemplateEntry) { 473 | return visitClassTemplateEntry( 474 | this, 475 | classTemplateEntry, 476 | getClassSpecializationByCursor(classTemplateEntry, cursor), 477 | ); 478 | } 479 | const typedefEntry = this.findTypedefByCursor(cursor); 480 | if (typedefEntry) { 481 | return visitTypedefEntry(this, typedefEntry.cursor); 482 | } 483 | const hasChildren = cursor.visitChildren(() => 484 | CXChildVisitResult.CXChildVisit_Break 485 | ); 486 | if (hasChildren) { 487 | throw new Error( 488 | `Unexpectedly found an unregistered class entry with children '${ 489 | getNamespacedName(cursor) 490 | }'`, 491 | ); 492 | } 493 | const specialized = cursor.getSpecializedTemplate(); 494 | if (!specialized || specialized.equals(cursor)) { 495 | throw new Error( 496 | `Unexpectedly found an unregistered class that did not specialize a template '${ 497 | getNamespacedName(cursor) 498 | }'`, 499 | ); 500 | } 501 | return this.visitClassLikeByCursor(specialized, importEntry); 502 | } 503 | 504 | visitFunction(importEntry: FunctionContent): void { 505 | const found = this.#functions.find((entry) => 506 | entry.name === importEntry.name || entry.nsName === importEntry.name 507 | ); 508 | if (!found) { 509 | throw new Error(`Could not find function '${importEntry.name}'`); 510 | } 511 | const result = visitFunctionCursor( 512 | this, 513 | found.cursor, 514 | ); 515 | 516 | found.parameters = result.parameters; 517 | found.result = result.result; 518 | found.used = true; 519 | } 520 | 521 | visitVar(importEntry: VarContent): void { 522 | const found = this.#vars.find((entry) => 523 | entry.name === importEntry.name || entry.nsName === importEntry.name 524 | ); 525 | if (!found) { 526 | throw new Error(`Could not find var '${importEntry.name}'`); 527 | } 528 | 529 | visitVarEntry( 530 | this, 531 | found, 532 | ); 533 | } 534 | 535 | pushToNamespaceStack(namespace: string) { 536 | this.#nsStack.push(namespace); 537 | } 538 | 539 | popFromNamespaceStack() { 540 | this.#nsStack.pop(); 541 | } 542 | 543 | findClassByCursor(cursor: CXCursor) { 544 | return this.#classes.find((entry) => 545 | entry.cursor.equals(cursor) || 546 | entry.forwardDeclarations.some((decl) => decl.equals(cursor)) 547 | ); 548 | } 549 | 550 | findClassByName(name: string) { 551 | const nsMatch = this.#classes.find((entry) => entry.nsName === name); 552 | if (nsMatch) { 553 | return nsMatch; 554 | } 555 | const nameMatches = this.#classes.filter((entry) => entry.name === name); 556 | if (nameMatches.length === 1) { 557 | return nameMatches[0]; 558 | } else if (nameMatches.length > 1) { 559 | throw new Error( 560 | `Searching for class by name produced multiple matches: Use namespaced name to narrow down the search`, 561 | ); 562 | } 563 | } 564 | 565 | findClassByType(type: CXType) { 566 | const declaration = type.getTypeDeclaration(); 567 | if (declaration) { 568 | return this.findClassByCursor(declaration); 569 | } else { 570 | const name = type.getSpelling(); 571 | return this.findClassByName( 572 | type.isConstQualifiedType() ? name.substring(6) : name, 573 | ); 574 | } 575 | } 576 | 577 | findClassTemplateByCursor(cursor: CXCursor) { 578 | return this.#classTemplates.find((entry) => 579 | entry.cursor.equals(cursor) || 580 | entry.partialSpecializations.some((spec) => spec.cursor.equals(cursor)) || 581 | entry.forwardDeclarations.some((decl) => decl.equals(cursor)) 582 | ); 583 | } 584 | 585 | findClassTemplateByName(name: string) { 586 | const nsMatch = this.#classTemplates.find((entry) => entry.nsName === name); 587 | if (nsMatch) { 588 | return nsMatch; 589 | } 590 | const nameMatches = this.#classTemplates.filter((entry) => 591 | entry.name === name 592 | ); 593 | if (nameMatches.length === 1) { 594 | return nameMatches[0]; 595 | } else if (nameMatches.length > 1) { 596 | throw new Error( 597 | `Searching for classtemplate by name produced multiple matches: Use namespaced name to narrow down the search`, 598 | ); 599 | } 600 | } 601 | 602 | findClassTemplateByType(type: CXType) { 603 | const declaration = type.getTypeDeclaration(); 604 | if (declaration) { 605 | return this.findClassTemplateByCursor(declaration); 606 | } else { 607 | const name = type.getSpelling(); 608 | return this.findClassTemplateByName( 609 | type.isConstQualifiedType() ? name.substring(6) : name, 610 | ); 611 | } 612 | } 613 | 614 | findUnionByCursor(cursor: CXCursor) { 615 | return this.#unions.find((entry) => entry.cursor.equals(cursor)); 616 | } 617 | 618 | findFunctionByCursor(cursor: CXCursor) { 619 | return this.#functions.find((entry) => entry.cursor.equals(cursor)); 620 | } 621 | 622 | findFunctionByName(name: string) { 623 | const nsMatch = this.#functions.find((entry) => entry.nsName === name); 624 | if (nsMatch) { 625 | return nsMatch; 626 | } 627 | const nameMatches = this.#functions.filter((entry) => entry.name === name); 628 | if (nameMatches.length === 1) { 629 | return nameMatches[0]; 630 | } else if (nameMatches.length > 1) { 631 | throw new Error( 632 | `Searching for function by name produced multiple matches: Use namespaced name to narrow down the search`, 633 | ); 634 | } 635 | } 636 | 637 | findFunctionByType(type: CXType) { 638 | const declaration = type.getTypeDeclaration(); 639 | if (declaration) { 640 | return this.findFunctionByCursor(declaration); 641 | } else { 642 | const name = type.getSpelling(); 643 | return this.findFunctionByName( 644 | type.isConstQualifiedType() ? name.substring(6) : name, 645 | ); 646 | } 647 | } 648 | 649 | findTypedefByCursor(cursor: CXCursor) { 650 | return this.#typedefs.find((entry) => entry.cursor.equals(cursor)); 651 | } 652 | 653 | findTypedefByName(name: string) { 654 | const nsMatch = this.#typedefs.find((entry) => entry.nsName === name); 655 | if (nsMatch) { 656 | return nsMatch; 657 | } 658 | const nameMatches = this.#typedefs.filter((entry) => entry.name === name); 659 | if (nameMatches.length === 1) { 660 | return nameMatches[0]; 661 | } else if (nameMatches.length > 1) { 662 | throw new Error( 663 | `Searching for typedef by name produced multiple matches: Use namespaced name to narrow down the search`, 664 | ); 665 | } 666 | } 667 | 668 | findTypedefByType(type: CXType) { 669 | const declaration = type.getTypeDeclaration(); 670 | if (declaration) { 671 | return this.findTypedefByCursor(declaration); 672 | } else { 673 | const name = type.getSpelling(); 674 | return this.findTypedefByName( 675 | type.isConstQualifiedType() ? name.substring(6) : name, 676 | ); 677 | } 678 | } 679 | 680 | getClasses() { 681 | return this.#classes; 682 | } 683 | 684 | getClassTemplates() { 685 | return this.#classTemplates; 686 | } 687 | 688 | getEnums() { 689 | return this.#enums; 690 | } 691 | 692 | getTypedefs() { 693 | return this.#typedefs; 694 | } 695 | 696 | getUsedData(): Map< 697 | AbsoluteFilePath, 698 | UseableEntry[] 699 | > { 700 | const map = new Map< 701 | AbsoluteFilePath, 702 | UseableEntry[] 703 | >(); 704 | 705 | for (const entry of this.#useableEntries) { 706 | if (!entry.used) { 707 | continue; 708 | } 709 | const fileEntries = map.get(entry.file) || 710 | map.set(entry.file, []).get(entry.file)!; 711 | 712 | if (entry.kind === "class") { 713 | renameClassTemplateSpecializations(entry); 714 | } 715 | if (entry.kind === "class" || entry.kind === "class") { 716 | replaceSelfReferentialFieldValues(entry); 717 | } 718 | 719 | fileEntries.push(entry); 720 | } 721 | 722 | return map; 723 | } 724 | } 725 | 726 | const replaceSelfReferentialFieldValues = ( 727 | source: ClassEntry | ClassTemplateEntry, 728 | ) => { 729 | const visitorCallback = (entry: null | TypeEntry) => { 730 | if (entry === source) { 731 | throw new Error("Class self-refers itself"); 732 | } 733 | if (typeof entry === "string" || !entry) { 734 | return; 735 | } 736 | if (isTypedef(entry)) { 737 | visitorCallback(entry.target); 738 | } else if (isPointer(entry)) { 739 | if (entry.pointee === "self" || entry.pointee === source) { 740 | entry.pointee = "self"; 741 | return; 742 | } 743 | visitorCallback(entry.pointee); 744 | } else if (isFunction(entry)) { 745 | entry.parameters.forEach((parameter) => visitorCallback(parameter.type)); 746 | visitorCallback(entry.result); 747 | } else if ( 748 | isStruct(entry) || isInlineStruct(entry) 749 | ) { 750 | entry.fields.forEach((field) => visitorCallback(field.type)); 751 | } else if (isConstantArray(entry)) { 752 | visitorCallback(entry.element); 753 | } else if (isUnion(entry)) { 754 | entry.fields.forEach(visitorCallback); 755 | } else if (isInlineTemplateStruct(entry)) { 756 | entry.parameters.forEach((param) => 757 | param.kind === "parameter" ? visitorCallback(param.type) : null 758 | ); 759 | if (!entry.specialization) { 760 | entry.specialization = entry.template.defaultSpecialization!; 761 | } 762 | entry.specialization.fields.forEach((field) => 763 | visitorCallback(field.type) 764 | ); 765 | } 766 | }; 767 | const cb = (field: ClassField) => { 768 | visitorCallback(field.type); 769 | }; 770 | if (source.kind === "class") { 771 | source.fields.forEach(cb); 772 | } else { 773 | if (source.defaultSpecialization) { 774 | source.defaultSpecialization.fields.forEach(cb); 775 | } 776 | source.partialSpecializations.forEach((spec) => spec.fields.forEach(cb)); 777 | } 778 | }; 779 | --------------------------------------------------------------------------------