From 92e57fbc8c66edb6442d5c419dc3edc00c193dda Mon Sep 17 00:00:00 2001 From: Divy Srivastava Date: Sun, 12 Nov 2023 00:37:21 -0800 Subject: [PATCH] new CLI, stateless macro and struct redesign (#136) --- .github/workflows/ci.yml | 18 +- .gitignore | 1 + .rustfmt.toml | 11 +- Cargo.lock | 454 +++++++++++++++++- Cargo.toml | 5 +- LICENSE | 3 +- Makefile | 14 +- README.md | 223 +++------ cli.ts | 99 ---- codegen.ts | 300 ------------ deno_bindgen/Cargo.toml | 5 + deno_bindgen/lib.rs | 66 +-- .../tests/compile_fail/impl_registration.rs | 15 + .../compile_fail/impl_registration.stderr | 12 + .../tests/compile_fail/struct_by_val.rs | 20 + .../tests/compile_fail/struct_by_val.stderr | 15 + .../tests/pass/constructor_override.rs | 22 + deno_bindgen/tests/pass/simple.rs | 63 +++ deno_bindgen/tests/ui.rs | 11 + deno_bindgen_cli/Cargo.toml | 22 + deno_bindgen_cli/cargo.rs | 92 ++++ deno_bindgen_cli/dlfcn.rs | 32 ++ deno_bindgen_cli/main.rs | 39 ++ deno_bindgen_ir/Cargo.toml | 20 + deno_bindgen_ir/codegen/deno.rs | 429 +++++++++++++++++ deno_bindgen_ir/codegen/mod.rs | 40 ++ deno_bindgen_ir/inventory.rs | 12 + deno_bindgen_ir/lib.rs | 220 +++++++++ deno_bindgen_macro/Cargo.toml | 11 +- deno_bindgen_macro/src/attrs.rs | 105 ---- deno_bindgen_macro/src/derive_fn.rs | 208 -------- deno_bindgen_macro/src/derive_struct.rs | 274 ----------- deno_bindgen_macro/src/docs.rs | 27 -- deno_bindgen_macro/src/fn_.rs | 215 +++++++++ deno_bindgen_macro/src/impl_.rs | 150 ++++++ deno_bindgen_macro/src/lib.rs | 282 +++-------- deno_bindgen_macro/src/meta.rs | 78 --- deno_bindgen_macro/src/struct_.rs | 26 + deno_bindgen_macro/src/util.rs | 26 + deno_bindgen_macro/tests/fn/add.test.out.rs | 19 + deno_bindgen_macro/tests/fn/add.test.rs | 1 + .../tests/fn/buffer.test.out.rs | 24 + deno_bindgen_macro/tests/fn/buffer.test.rs | 7 + .../tests/fn/pointer.test.out.rs | 21 + deno_bindgen_macro/tests/fn/pointer.test.rs | 3 + e2e_test/Cargo.lock | 203 ++++++++ e2e_test/Cargo.toml | 13 + e2e_test/bench.js | 12 + e2e_test/bindings/mod.ts | 301 ++++++++++++ e2e_test/bindings_test.ts | 114 +++++ e2e_test/src/lib.rs | 87 ++++ example/Cargo.toml | 7 +- example/README.md | 13 + example/bench.js | 12 - example/bindings/bindings.ts | 327 ------------- example/bindings_test.ts | 254 ---------- example/lib.rs | 71 +++ example/lsusb.ts | 4 + example/mod.ts | 203 ++++++++ example/src/lib.rs | 234 --------- 60 files changed, 3209 insertions(+), 2386 deletions(-) delete mode 100644 cli.ts delete mode 100644 codegen.ts create mode 100644 deno_bindgen/tests/compile_fail/impl_registration.rs create mode 100644 deno_bindgen/tests/compile_fail/impl_registration.stderr create mode 100644 deno_bindgen/tests/compile_fail/struct_by_val.rs create mode 100644 deno_bindgen/tests/compile_fail/struct_by_val.stderr create mode 100644 deno_bindgen/tests/pass/constructor_override.rs create mode 100644 deno_bindgen/tests/pass/simple.rs create mode 100644 deno_bindgen/tests/ui.rs create mode 100644 deno_bindgen_cli/Cargo.toml create mode 100644 deno_bindgen_cli/cargo.rs create mode 100644 deno_bindgen_cli/dlfcn.rs create mode 100644 deno_bindgen_cli/main.rs create mode 100644 deno_bindgen_ir/Cargo.toml create mode 100644 deno_bindgen_ir/codegen/deno.rs create mode 100644 deno_bindgen_ir/codegen/mod.rs create mode 100644 deno_bindgen_ir/inventory.rs create mode 100644 deno_bindgen_ir/lib.rs delete mode 100644 deno_bindgen_macro/src/attrs.rs delete mode 100644 deno_bindgen_macro/src/derive_fn.rs delete mode 100644 deno_bindgen_macro/src/derive_struct.rs delete mode 100644 deno_bindgen_macro/src/docs.rs create mode 100644 deno_bindgen_macro/src/fn_.rs create mode 100644 deno_bindgen_macro/src/impl_.rs delete mode 100644 deno_bindgen_macro/src/meta.rs create mode 100644 deno_bindgen_macro/src/struct_.rs create mode 100644 deno_bindgen_macro/src/util.rs create mode 100644 deno_bindgen_macro/tests/fn/add.test.out.rs create mode 100644 deno_bindgen_macro/tests/fn/add.test.rs create mode 100644 deno_bindgen_macro/tests/fn/buffer.test.out.rs create mode 100644 deno_bindgen_macro/tests/fn/buffer.test.rs create mode 100644 deno_bindgen_macro/tests/fn/pointer.test.out.rs create mode 100644 deno_bindgen_macro/tests/fn/pointer.test.rs create mode 100644 e2e_test/Cargo.lock create mode 100644 e2e_test/Cargo.toml create mode 100644 e2e_test/bench.js create mode 100644 e2e_test/bindings/mod.ts create mode 100644 e2e_test/bindings_test.ts create mode 100644 e2e_test/src/lib.rs create mode 100644 example/README.md delete mode 100644 example/bench.js delete mode 100644 example/bindings/bindings.ts delete mode 100644 example/bindings_test.ts create mode 100644 example/lib.rs create mode 100644 example/lsusb.ts create mode 100644 example/mod.ts delete mode 100644 example/src/lib.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7062169..f76dd15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: matrix: os: [ windows-latest, macos-latest, ubuntu-latest ] toolchain: [nightly] - deno_version: [1.33.1] + deno_version: [1.38.1] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 @@ -35,15 +35,19 @@ jobs: components: rustfmt, clippy - name: Build run: cargo build --locked --release - - name: Build Example (debug) - working-directory: ./example + - name: Test (debug) + working-directory: ./e2e_test run: | - deno run -A ../cli.ts + ../target/release/deno_bindgen -o bindings/mod.ts deno test -A --unstable - - name: Build Example (release) - working-directory: ./example + - name: Test (release) + working-directory: ./e2e_test shell: bash run: | rm -rf target - deno run -A ../cli.ts --release=../target/release + ../target/release/deno_bindgen -o bindings/mod.ts --release deno test -A --unstable + - name: Bench + working-directory: ./e2e_test + shell: bash + run: deno bench -A --unstable diff --git a/.gitignore b/.gitignore index 202f18c..edfb2f6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ target/ example/Cargo.lock bindings.json .DS_Store +deno.lock \ No newline at end of file diff --git a/.rustfmt.toml b/.rustfmt.toml index de76f84..d039a95 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,11 +1,4 @@ -edition = "2021" max_width = 80 tab_spaces = 2 -unstable_features = true -condense_wildcard_suffixes = true -newline_style = "Unix" -use_field_init_shorthand = true -use_try_shorthand = true -imports_granularity = "Crate" -group_imports = "StdExternalCrate" -reorder_imports = true +edition = "2021" +imports_granularity = "Item" \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 9dd1519..8407e0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,13 +21,123 @@ dependencies = [ "memchr", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "basic-toml" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f2139706359229bfa8f19142ac1155b4b80beafb7a60471ac5dd109d4a19778" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "camino" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12024c4645c97566567129c204f65d5815a8c9aecf30fcbe682b2fe034996d36" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + [[package]] name = "deno_bindgen" version = "0.8.1" dependencies = [ + "deno_bindgen_ir", "deno_bindgen_macro", + "linkme", "serde", "serde_json", + "trybuild", +] + +[[package]] +name = "deno_bindgen_cli" +version = "0.1.0" +dependencies = [ + "cargo_metadata", + "deno_bindgen_ir", + "dlopen2", + "structopt", +] + +[[package]] +name = "deno_bindgen_ir" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", ] [[package]] @@ -35,18 +145,68 @@ name = "deno_bindgen_macro" version = "0.8.1" dependencies = [ "Inflector", + "deno_bindgen_ir", + "prettyplease", "proc-macro2", "quote", "serde", "serde_json", - "syn", + "syn 2.0.39", + "testing_macros", +] + +[[package]] +name = "dlopen2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bc2c7ed06fd72a8513ded8d0d2f6fd2655a85d6885c48cae8625d80faf28c03" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", ] [[package]] name = "itoa" -version = "0.4.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "lazy_static" @@ -54,26 +214,103 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "libc" +version = "0.2.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" + +[[package]] +name = "linkme" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ed2ee9464ff9707af8e9ad834cffa4802f072caad90639c583dd3c62e6e608" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba125974b109d512fccbc6c0244e7580143e460895dfd6ea7f8bbb692fd94396" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "memchr" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "pmutil" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "prettyplease" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" +dependencies = [ + "proc-macro2", + "syn 2.0.39", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.74", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" -version = "1.0.28" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "quote" -version = "1.0.9" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -95,43 +332,88 @@ version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +[[package]] +name = "relative-path" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c707298afce11da2efef2f600116fa93ffa7a032b5d7b628aa17711ec81383ca" + [[package]] name = "ryu" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[package]] +name = "semver" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +dependencies = [ + "serde", +] + [[package]] name = "serde" -version = "1.0.127" +version = "1.0.192" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8" +checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.127" +version = "1.0.192" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc" +checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.39", ] [[package]] name = "serde_json" -version = "1.0.66" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", "serde", ] +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "structopt" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" +dependencies = [ + "clap", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.74", +] + [[package]] name = "syn" version = "1.0.74" @@ -143,8 +425,150 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "syn" +version = "2.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "testing_macros" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c15b796025051a07f1ac695ee0cac0883f05a0d510c9d171ef8d31a992e6a5" +dependencies = [ + "anyhow", + "glob", + "once_cell", + "pmutil", + "proc-macro2", + "quote", + "regex", + "relative-path", + "syn 2.0.39", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "trybuild" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "196a58260a906cedb9bf6d8034b6379d0c11f552416960452f267402ceeddff1" +dependencies = [ + "basic-toml", + "glob", + "once_cell", + "serde", + "serde_derive", + "serde_json", + "termcolor", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + [[package]] name = "unicode-xid" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index 1646b64..d37f4f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,9 @@ [workspace] +resolver = "2" members = [ "deno_bindgen_macro", "deno_bindgen", + "deno_bindgen_ir", + "deno_bindgen_cli" ] -exclude = ["example/"] \ No newline at end of file +exclude = ["e2e_test/", "example/"] \ No newline at end of file diff --git a/LICENSE b/LICENSE index 07bf05b..89e76f6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,6 @@ MIT License -Copyright (c) 2021-2022 Divy Srivastava -Copyright (c) 2022 the Deno authors +Copyright (c) 2023 Divy Srivastava Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index f40683e..49b61df 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,13 @@ fmt: cargo fmt - deno fmt --ignore=target/,example/target/,example/bindings/ + deno fmt --ignore=target/,e2e_test/target/,e2e_test/bindings/,example/target/ -test: - cd example && deno run -A ../cli.ts && deno test -A --unstable +build: + cargo build -bench: - cd example && deno run -A ../cli.ts && deno bench -A --unstable bench.js +test: build + cargo test + cd e2e_test && ../target/debug/deno_bindgen -o bindings/mod.ts && deno test -A --unstable + +bench: build + cd e2e_test && ../target/debug/deno_bindgen -o bindings/mod.ts && deno bench -A --unstable bench.js diff --git a/README.md b/README.md index 3f4dd65..3e5427a 100644 --- a/README.md +++ b/README.md @@ -1,199 +1,110 @@ -## `deno_bindgen` +# `deno_bindgen` - + This tool aims to simplify glue code generation for Deno FFI libraries written in Rust. -### QuickStart +## Install -Annotate on top of Rust `fn`, `struct` and `enum` to make them available to Deno. +Install the command-line via `cargo`: + +```bash +cargo install deno_bindgen_cli +``` + +## Usage ```rust -// add.rs use deno_bindgen::deno_bindgen; +// Export `add` function to JavaScript. #[deno_bindgen] -pub struct Input { - a: i32, - b: i32, -} - -#[deno_bindgen] -fn add(input: Input) -> i32 { - input.a + input.b +fn add(a: u32, b: u32) -> u32 { + a + b } ``` -Invoke the CLI to compile and generate bindings: - -```shell -$ deno_bindgen -``` - -And finally import the generated bindings in your JS +Use the exported functions directly in ESM with TypeScript typings ```typescript -// add.ts -import { add } from "./bindings/bindings.ts"; +import { add } from "./bindings/mod.ts"; -add({ a: 1, b: 2 }); // 3 +add(1, 2); ``` -### Installation +## Design -- Install the `deno_bindgen` CLI with Deno. +The tool is designed to make it very easy to write high performance FFI +bindings. A lot of the things have been redesigned in `0.10` to prevent perf +footguns. -```shell -deno install -Afrq -n deno_bindgen https://deno.land/x/deno_bindgen/cli.ts -``` +TypeScript types are generated and supported OOTB. -Add the following dependencies to your crate. - -```toml -# Cargo.toml -[dependencies] -deno_bindgen = "0.8.1" -serde = { version = "1", features = ["derive"] } -``` - -Change your `crate-type` to `cdylib` and set your package name as well. - -```toml -[lib] -name = "___" -crate-type = ["cdylib"] -``` - -### Bindings - -Put `#[deno_bindgen]` on top of a "serde-deriavable" struct, enum or fn. - -#### `struct` (named fields) - -These transform into Typescript `type`s. +All class handles support disposing memory via the Explicit Resource Management +API (`using`). ```rust -// lib.rs #[deno_bindgen] -pub struct A { - b: Vec>, -} -``` +pub struct Foo; -becomes: +#[deno_bindgen] +impl Foo { + #[constructor] + pub fn new() -> Self { + Self + } -```typescript -// bindings/bindings.ts -export type A = { - b: Array>; -}; + pub fn bar(&self) { + // ... + } +} ``` -#### `enum` +```js +import { Foo } from "@ffi/example"; -Enums become `type` unions in Typescript. - -```rust -#[deno_bindgen] -pub enum Event { - Quit, - MouseMove { - x: i32, - y: i32, - } +{ + using foo = new Foo(); + foo.bar(); + // foo is disposed here... } ``` -becomes: +High performance. Codegen tries its best to take the fastest possible path for +all bindings as-if they were written by hand to properly leverage the power of +the Deno FFI JIT calls. -```typescript -export type Enum = - | "quit" - | { - mouse_move: { - x: number; - y: number; - }; - }; +``` +> make bench +cpu: Apple M1 +runtime: deno 1.38.0 (aarch64-apple-darwin) + +file:///Users/divy/gh/deno_bindgen/example/bench.js +benchmark time (avg) iter/s (min … max) p75 p99 p995 +--------------------------------------------------------------- ----------------------------- +add 6.88 ns/iter 145,297,626.6 (6.78 ns … 13.33 ns) 6.81 ns 8.22 ns 9.4 ns +bytelen 8.05 ns/iter 124,278,976.3 (7.81 ns … 18.1 ns) 8.09 ns 10.39 ns 11.64 ns ``` -### `fn` +## Publishing -Functions are exposed through the FFI boundaries. +By default, deno_bindgen generates bindings for local development. To publish a +cross-platform binding, you can use the `--lazy-init` flag, this gives you full +control on how you want to host pre-built shared libraries and pull them in at +runtime. -```rust -#[deno_bindgen] -fn greet(name: &str) { - println!("Hello, {}!", name); -} +```bash +deno_bindgen --release --lazy-init ``` -becomes: - ```typescript -export function greet(name: string) { - // ... glue code for calling the - // symbol. -} -``` - -Notes +import { add, load } from "./example/mod.ts"; +import { cache } from "https://deno.land/x/cache/mod.ts"; -- Use `#[deno_bindgen(non_blocking)]` attribute to call symbol without blocking - JS event loop. Exposed as an async funtion from bindings. +// Download the shared library from a CDN +const file = await cache("https://example.com/example.so"); +load(file.path); -- Rust doc comments transform to JS docs. - ```rust - #[deno_bindgen] - pub struct Me { - /// My name... - /// ...it is - name: String, - } - ``` - becomes: - ```typescript - export type Me = { - /** - * My name... - * ...it is - */ - name: string; - }; - ``` - -- If the argument type of Rust is f32, the calculation result may be different.\ - Number in Java Script is float64, when data is passed to Rust, it becomes - float32, so the number may change.\ - e.g: `1.3 + 1.5` will be `2.799999952316284` - -### CLI - -The `deno_bindgen` CLI tool provides the following flags: - -- Pass `--release` to create a release build. - -- `--release=URL` will load library artifacts from a remote location. This is - useful for updating bindings for end users after a release: - - ```shell - deno_bindgen --release=https://github.com/littledivy/deno_sdl2/releases/download/0.2-alpha.1 - ``` - - Under the hood this uses [`x/plug`](https://deno.land/x/plug) to fetch and - cache the artifact. - - Artifacts must be following the remote asset naming scheme, as follows: - - | OS | Arch | Naming | - | ------- | ------ | ------------------- | - | Windows | x86_64 | name.dll | - | Linux | x86_64 | libname.so | - | MacOS | x86_64 | libname.dylib | - | MacOS | arm64 | libname_arm64.dylib | - -- Flags after `--` will be passed to `cargo build`. Example: - ```shell - deno_bindgen -- --features "cool_stuff" - ``` +add(1, 2); +``` diff --git a/cli.ts b/cli.ts deleted file mode 100644 index 620dfe0..0000000 --- a/cli.ts +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright 2020-2021 the Deno authors. All rights reserved. MIT license. - -import { ensureDir } from "https://deno.land/std@0.132.0/fs/ensure_dir.ts"; -import { parse } from "https://deno.land/std@0.132.0/flags/mod.ts"; -import { join } from "https://deno.land/std@0.132.0/path/mod.ts"; -import { relative } from "https://deno.land/std@0.132.0/path/mod.ts"; -import { codegen } from "./codegen.ts"; - -const flags = parse(Deno.args, { "--": true }); -const release = !!flags.release; - -const metafile = join( - Deno.env.get("OUT_DIR") || await findRelativeTarget(), - "bindings.json", -); - -function build() { - const args = ["build"]; - if (release) args.push("--release"); - args.push(...flags["--"]); - console.log( - "[deno_bindgen] Command:", - ["cargo", ...args].map((s) => JSON.stringify(s)).join(" "), - ); - const proc = new Deno.Command("cargo", { args, stderr: "inherit" }); - return proc.output(); -} - -async function findRelativeTarget() { - const p = new Deno.Command("cargo", { - args: ["metadata", "--format-version", "1"], - stdout: "piped", - }); - const output = await p.output(); - const metadata = JSON.parse(new TextDecoder().decode(output.stdout)); - return relative(Deno.cwd(), metadata.workspace_root); -} - -let source = null; -async function generate() { - let conf; - try { - conf = JSON.parse(await Deno.readTextFile(metafile)); - } catch (_) { - console.log(`[deno_bindgen] metafile not found at '${metafile}'`); - // Nothing to update. - return; - } - - let cargoTarget = Deno.env.get("CARGO_TARGET_DIR"); - if (!cargoTarget) cargoTarget = "../target"; - - const pkgName = conf.name; - const fetchPrefix = typeof flags.release == "string" - ? flags.release - : await findRelativeTarget() + [cargoTarget, release ? "release" : "debug"] - .join("/"); - - source = "// Auto-generated with deno_bindgen\n"; - source += codegen( - fetchPrefix, - pkgName, - conf.typeDefs, - conf.tsTypes, - conf.symbols, - { - le: conf.littleEndian, - release, - releaseURL: flags.release, - }, - ); - - await Deno.remove(metafile); -} - -try { - await Deno.remove(metafile); -} catch { - // no op -} - -console.log("[deno_bindgen] Building..."); -const status = await build().catch((_) => Deno.removeSync(metafile)); -if (status?.success || typeof flags.release == "string") { - await generate(); - if (source) { - await ensureDir("bindings"); - await Deno.writeTextFile("bindings/bindings.ts", source); - console.log("[deno_bindgen] Written at 'bindings/bindings.ts'"); - } else { - console.log( - "[deno_bindgen] bindings.ts not generated: nothing to generate", - ); - } -} else { - console.log("[deno_bindgen] Build failed with code", status?.code); -} - -Deno.exit(status?.code || 0); diff --git a/codegen.ts b/codegen.ts deleted file mode 100644 index f7a9b96..0000000 --- a/codegen.ts +++ /dev/null @@ -1,300 +0,0 @@ -// deno-lint-ignore-file no-explicit-any -// Copyright 2020-2021 the Deno authors. All rights reserved. MIT license. - -import { - createFromBuffer, - GlobalConfiguration, -} from "https://deno.land/x/dprint@0.2.0/mod.ts"; -import * as Cache from "https://deno.land/x/cache@0.2.13/mod.ts"; - -Cache.configure({ directory: Cache.options.directory }); -const cache = Cache.namespace("deno_bindgen_cli"); - -const globalConfig: GlobalConfiguration = { - indentWidth: 2, - lineWidth: 80, -}; - -const file = await cache.cache( - "https://plugins.dprint.dev/typescript-0.57.0.wasm", -); - -const tsFormatter = createFromBuffer(Deno.readFileSync(file.path)); - -tsFormatter.setConfig(globalConfig, { - semiColons: "asi", -}); - -const Type: Record = { - void: "null", - i8: "number", - u8: "number", - i16: "number", - u16: "number", - i32: "number", - u32: "number", - i64: "bigint", - u64: "bigint", - usize: "bigint", - isize: "bigint", - f32: "number", - f64: "number", -}; - -const BufferTypes: Record = { - str: "string", - buffer: "Uint8Array", - buffermut: "Uint8Array", - ptr: "Uint8Array", -}; - -enum Encoder { - JsonStringify = "JSON.stringify", - None = "", -} - -const BufferTypeEncoders: Record = { - str: Encoder.None, - buffer: Encoder.None, - buffermut: Encoder.None, - ptr: Encoder.None, -}; - -type TypeDef = Record>; - -function resolveType(typeDefs: TypeDef, type: any): string { - const t = typeof type == "string" ? type : type.structenum.ident; - if (Type[t] !== undefined) return Type[t]; - if (BufferTypes[t] !== undefined) return BufferTypes[t]; - if (Object.keys(typeDefs).find((f) => f == t) !== undefined) { - return t; - } - return "any"; -} - -function resolveDlopenParameter(typeDefs: TypeDef, type: any): string { - const t = typeof type == "string" ? type : type.structenum.ident; - if (Type[t] !== undefined) return t; - if (BufferTypes[t] !== undefined) { - return "buffer"; - } - if ( - Object.keys(typeDefs).find((f) => f == t) !== undefined - ) { - return "buffer"; - } else { - return "pointer"; - } - // deno-lint-ignore no-unreachable - throw new TypeError(`Type not supported: ${t}`); -} - -type Sig = Record< - string, - { - parameters: any[]; - result: string; - nonBlocking?: boolean; - } ->; - -type Options = { - le?: boolean; - release?: boolean; - releaseURL: string | undefined; -}; - -function isTypeDef(p: any) { - return typeof p !== "string"; -} - -function isBufferType(p: any) { - return isTypeDef(p) || BufferTypes[p] !== undefined; -} - -function needsPointer(p: any) { - return isBufferType(p) && p !== "buffer" && p !== "buffermut"; -} - -// TODO(@littledivy): factor out options in an interface -export function codegen( - fetchPrefix: string, - name: string, - decl: TypeDef, - typescript: Record, - signature: Sig, - options?: Options, -) { - signature = Object.keys(signature) - .sort() - .reduce( - (acc, key) => ({ - ...acc, - [key]: signature[key], - }), - {}, - ); - - return tsFormatter.formatText( - "bindings.ts", - ` -function encode(v: string | Uint8Array): Uint8Array { - if (typeof v !== "string") return v; - return new TextEncoder().encode(v); -} - -function decode(v: Uint8Array): string { - return new TextDecoder().decode(v); -} - -// deno-lint-ignore no-explicit-any -function readPointer(v: any): Uint8Array { - const ptr = new Deno.UnsafePointerView(v); - const lengthBe = new Uint8Array(4); - const view = new DataView(lengthBe.buffer); - ptr.copyInto(lengthBe, 0); - const buf = new Uint8Array(view.getUint32(0)); - ptr.copyInto(buf, 4); - return buf; -} - -const url = new URL("${fetchPrefix}", import.meta.url); -${ - typeof options?.releaseURL === "string" - ? ` -import { dlopen, FetchOptions } from "https://deno.land/x/plug@1.0.1/mod.ts"; -let uri = url.toString(); -if (!uri.endsWith("/")) uri += "/"; - -let darwin: string | { aarch64: string; x86_64: string } = uri; - -const opts: FetchOptions = { - name: "${name}", - url: { - darwin, - windows: uri, - linux: uri, - }, - suffixes: { - darwin: { - aarch64: "_arm64", - }, - }, - cache: ${!!options?.release ? '"use"' : '"reloadAll"'}, -}; -const { symbols } = await dlopen(opts, { - ` - : ` -let uri = url.pathname; -if (!uri.endsWith("/")) uri += "/"; - -// https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibrarya#parameters -if (Deno.build.os === "windows") { - uri = uri.replace(/\\//g, "\\\\"); - // Remove leading slash - if (uri.startsWith("\\\\")) { - uri = uri.slice(1); - } -} - -const { symbols } = Deno.dlopen({ - darwin: uri + "lib${name}.dylib", - windows: uri + "${name}.dll", - linux: uri + "lib${name}.so", - freebsd: uri + "lib${name}.so", - netbsd: uri + "lib${name}.so", - aix: uri + "lib${name}.so", - solaris: uri + "lib${name}.so", - illumos: uri + "lib${name}.so", -}[Deno.build.os], {` - } - ${ - Object.keys(signature) - .map( - (sig) => - `${sig}: { parameters: [ ${ - signature[sig].parameters - .map((p) => { - const ffiParam = resolveDlopenParameter(decl, p); - // FIXME: Dupe logic here. - return `"${ffiParam}"${isBufferType(p) ? `, "usize"` : ""}`; - }) - .join(", ") - } ], result: "${ - resolveDlopenParameter( - decl, - signature[sig].result, - ) - }", nonblocking: ${String(!!signature[sig].nonBlocking)} }`, - ) - .join(", ") - } }); -${ - Object.keys(decl) - .sort() - .map((def) => typescript[def]) - .join("\n") - } -${ - Object.keys(signature) - .map((sig) => { - const { parameters, result, nonBlocking } = signature[sig]; - - return `export function ${sig}(${ - parameters - .map((p, i) => `a${i}: ${resolveType(decl, p)}`) - .join(",") - }) { - ${ - parameters - .map((p, i) => - isBufferType(p) - ? `const a${i}_buf = encode(${ - BufferTypeEncoders[p] ?? Encoder.JsonStringify - }(a${i}));` - : null - ) - .filter((c) => c !== null) - .join("\n") - } - - const rawResult = symbols.${sig}(${ - parameters - .map((p, i) => (isBufferType(p) - ? `a${i}_buf, a${i}_buf.byteLength` - : `a${i}`) - ) - .join(", ") - }); - ${ - isBufferType(result) - ? nonBlocking - ? `const result = rawResult.then(readPointer);` - : `const result = readPointer(rawResult);` - : "const result = rawResult;" - }; - ${ - isTypeDef(result) - ? nonBlocking - ? `return result.then(r => JSON.parse(decode(r))) as Promise<${ - resolveType( - decl, - result, - ) - }>;` - : `return JSON.parse(decode(result)) as ${ - resolveType(decl, result) - };` - : result == "str" - ? nonBlocking - ? "return result.then(decode);" - : "return decode(result);" - : "return result;" - }; -}`; - }) - .join("\n") - } - `, - ); -} diff --git a/deno_bindgen/Cargo.toml b/deno_bindgen/Cargo.toml index a58989c..df535ad 100644 --- a/deno_bindgen/Cargo.toml +++ b/deno_bindgen/Cargo.toml @@ -16,5 +16,10 @@ path = "./lib.rs" [dependencies] deno_bindgen_macro = { path = "../deno_bindgen_macro", version = "0.8.1" } +deno_bindgen_ir = { path = "../deno_bindgen_ir", version = "0.1.0" } serde = { version = "1", features = ["derive"] } serde_json = "1" +linkme = "0.3" + +[dev-dependencies] +trybuild = "1.0.85" \ No newline at end of file diff --git a/deno_bindgen/lib.rs b/deno_bindgen/lib.rs index d181f8f..e4b6e1f 100644 --- a/deno_bindgen/lib.rs +++ b/deno_bindgen/lib.rs @@ -1,53 +1,19 @@ // Copyright 2020-2021 the Deno authors. All rights reserved. MIT license. - -//! ## deno_bindgen -//! This tool aims to simply types & glue code generation for FFI -//! libraries written in Rust. -//! -//! ### Usage -//! Add `serde` and `deno_bindgen` dependency to your crate. -//! -//! ``` -//! use deno_bindgen::deno_bindgen; -//! -//! #[deno_bindgen] -//! pub struct Input { -//! /// Doc comments are transformed into -//! /// jsdocs. -//! a: Vec>, -//! } -//! -//! #[deno_bindgen(non_blocking)] -//! pub fn say_hello(message: &str) { -//! println!("{}", message); -//! } -//! ``` -//! -//! Generated bindings will look like this: -//! ``` -//! // bindings/binding.ts -//! -//! // ... -//! -//! type Input = { -//! /** -//! * Doc comments are transformed into -//! * jsdocs. -//! **/ -//! a: Array>; -//! }; -//! -//! export async function say_hello(message: string) { -//! // ... -//! } -//! ``` -//! These bindings contain nessecary code to open the shared library, -//! define symbols and expose type definitions. -//! They can be simply imported into Deno code: -//! ``` -//! import { say_hello } from "./bindings/bindings.ts"; -//! await say_hello("Demn!") -//! ``` -//! pub use ::serde_json; +use deno_bindgen_ir::codegen::Options; +pub use deno_bindgen_ir::*; pub use deno_bindgen_macro::deno_bindgen; +pub use linkme; +use linkme::distributed_slice; + +#[distributed_slice] +pub static INVENTORY: [Inventory]; + +pub trait BindgenType { + fn type_name() -> &'static str; +} + +#[no_mangle] +fn init_deno_bindgen(opt: Options) { + deno_bindgen_ir::codegen::generate(&INVENTORY, opt).unwrap(); +} diff --git a/deno_bindgen/tests/compile_fail/impl_registration.rs b/deno_bindgen/tests/compile_fail/impl_registration.rs new file mode 100644 index 0000000..607c6fb --- /dev/null +++ b/deno_bindgen/tests/compile_fail/impl_registration.rs @@ -0,0 +1,15 @@ +use deno_bindgen::deno_bindgen; + +// struct Foo is not "registered" in the inventory, so it `impl Foo` +// is not allowed. +struct Foo; + +#[deno_bindgen] +impl Foo { + #[constructor] + fn new() -> Foo { + Foo + } +} + +fn main() {} \ No newline at end of file diff --git a/deno_bindgen/tests/compile_fail/impl_registration.stderr b/deno_bindgen/tests/compile_fail/impl_registration.stderr new file mode 100644 index 0000000..19ae001 --- /dev/null +++ b/deno_bindgen/tests/compile_fail/impl_registration.stderr @@ -0,0 +1,12 @@ +error[E0277]: the trait bound `Foo: BindgenType` is not satisfied + --> tests/compile_fail/impl_registration.rs:8:6 + | +8 | impl Foo { + | ^^^ the trait `BindgenType` is not implemented for `Foo` + | +note: required by a bound in `_assert_impl` + --> tests/compile_fail/impl_registration.rs:7:1 + | +7 | #[deno_bindgen] + | ^^^^^^^^^^^^^^^ required by this bound in `_assert_impl` + = note: this error originates in the attribute macro `deno_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/deno_bindgen/tests/compile_fail/struct_by_val.rs b/deno_bindgen/tests/compile_fail/struct_by_val.rs new file mode 100644 index 0000000..207c7ed --- /dev/null +++ b/deno_bindgen/tests/compile_fail/struct_by_val.rs @@ -0,0 +1,20 @@ +use deno_bindgen::deno_bindgen; + +#[deno_bindgen] +struct Foo; + +#[deno_bindgen] +impl Foo { + #[constructor] + fn new() -> Foo { + Foo + } +} + +#[deno_bindgen] +fn foo(_foo: Foo) {} // Fail + +#[deno_bindgen] +fn foo2(_foo: &mut Foo) {} // Pass + +fn main() {} \ No newline at end of file diff --git a/deno_bindgen/tests/compile_fail/struct_by_val.stderr b/deno_bindgen/tests/compile_fail/struct_by_val.stderr new file mode 100644 index 0000000..d5021a3 --- /dev/null +++ b/deno_bindgen/tests/compile_fail/struct_by_val.stderr @@ -0,0 +1,15 @@ +error[E0308]: mismatched types + --> tests/compile_fail/struct_by_val.rs:15:8 + | +15 | fn foo(_foo: Foo) {} // Fail + | --- ^^^^ expected `Foo`, found `&mut _` + | | + | arguments to this function are incorrect + | + = note: expected struct `Foo` + found mutable reference `&mut _` +note: function defined here + --> tests/compile_fail/struct_by_val.rs:15:4 + | +15 | fn foo(_foo: Foo) {} // Fail + | ^^^ --------- diff --git a/deno_bindgen/tests/pass/constructor_override.rs b/deno_bindgen/tests/pass/constructor_override.rs new file mode 100644 index 0000000..92392df --- /dev/null +++ b/deno_bindgen/tests/pass/constructor_override.rs @@ -0,0 +1,22 @@ +use deno_bindgen::deno_bindgen; + +#[deno_bindgen] +struct Input { + a: i32, + b: i32, +} + +#[deno_bindgen] +impl Input { + #[constructor] + fn new(a: i32, b: i32) -> Input { + Input { a, b } + } + + #[constructor] + fn new2() -> Input { + Input { a: 0, b: 0 } + } +} + +fn main() {} \ No newline at end of file diff --git a/deno_bindgen/tests/pass/simple.rs b/deno_bindgen/tests/pass/simple.rs new file mode 100644 index 0000000..a646466 --- /dev/null +++ b/deno_bindgen/tests/pass/simple.rs @@ -0,0 +1,63 @@ +use deno_bindgen_macro::deno_bindgen; + +#[deno_bindgen] +fn add(a: i32, b: i32) -> i32 { + a + b +} + +#[deno_bindgen] +fn buf_mut(b: &mut [u8]) { + b[0] = 99; +} + +#[deno_bindgen] +fn cstr() -> *const u8 { + b"Hello, World!\0".as_ptr() +} + +#[deno_bindgen] +fn strlen(s: *const u8) -> u32 { + let mut len = 0; + unsafe { + while *s.add(len as usize) != 0 { + len += 1; + } + } + len +} + +#[deno_bindgen(non_blocking)] +fn non_blocking() -> i32 { + 42 +} + +#[deno_bindgen] +struct Foo { + internal: i32, +} + +#[deno_bindgen] +impl Foo { + #[constructor] + fn new(internal: i32) -> Foo { + Foo { internal } + } + + fn bar(&self) -> i32 { + 42 + } + + fn baz(&self, a: i32) -> i32 { + a + } + + fn qux(&self, a: i32, b: i32) -> i32 { + a + b + } + + fn quux(&mut self) { + self.internal += 1; + } +} + +fn main() {} \ No newline at end of file diff --git a/deno_bindgen/tests/ui.rs b/deno_bindgen/tests/ui.rs new file mode 100644 index 0000000..688afe2 --- /dev/null +++ b/deno_bindgen/tests/ui.rs @@ -0,0 +1,11 @@ +#[test] +fn ui_pass() { + let t = trybuild::TestCases::new(); + t.pass("tests/pass/*.rs"); +} + +#[test] +fn ui_compile_fail() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/compile_fail/*.rs"); +} diff --git a/deno_bindgen_cli/Cargo.toml b/deno_bindgen_cli/Cargo.toml new file mode 100644 index 0000000..b71d64c --- /dev/null +++ b/deno_bindgen_cli/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "deno_bindgen_cli" +version = "0.1.0" +description = "This tool aims to simplify glue code generation for Deno FFI libraries written in Rust." +documentation = "https://docs.rs/deno_bindgen" +homepage = "https://github.com/denoland/deno_bindgen" +repository = "https://github.com/denoland/deno_bindgen" +keywords = ["deno", "ffi", "bindgen", "bindings", "macro"] +categories = ["development-tools::ffi", "development-tools"] +readme = "../README.md" +license = "MIT" +edition = "2021" + +[[bin]] +name = "deno_bindgen" +path = "./main.rs" + +[dependencies] +structopt = "0.3.26" +deno_bindgen_ir = { path = "../deno_bindgen_ir", version = "0.1.0" } +dlopen2 = "0.6.1" +cargo_metadata = "0.18.1" \ No newline at end of file diff --git a/deno_bindgen_cli/cargo.rs b/deno_bindgen_cli/cargo.rs new file mode 100644 index 0000000..cca48fc --- /dev/null +++ b/deno_bindgen_cli/cargo.rs @@ -0,0 +1,92 @@ +use std::io::Result; +use std::path::Path; +use std::path::PathBuf; +use std::process::Command; +use std::process::Stdio; + +pub struct Artifact { + pub path: PathBuf, + pub manifest_path: PathBuf, +} + +#[derive(Default)] +pub struct Build { + release: bool, +} + +impl Build { + pub fn new() -> Self { + Self::default() + } + + pub fn release(mut self, release: bool) -> Self { + self.release = release; + self + } + + pub fn build(self, path: &Path) -> Result { + let mut cmd = Command::new("cargo"); + cmd + .current_dir(path) + .arg("build") + .arg("--lib") + .arg("--message-format=json") + .stdout(Stdio::piped()); + + if self.release { + cmd.arg("--release"); + } + + let status = cmd.status()?; + let output = cmd.output()?; + if status.success() { + let reader = std::io::BufReader::new(output.stdout.as_slice()); + let mut artifacts = vec![]; + for message in cargo_metadata::Message::parse_stream(reader) { + match message.unwrap() { + cargo_metadata::Message::CompilerArtifact(artifact) => { + if artifact.target.kind.contains(&"cdylib".to_string()) { + artifacts.push(Artifact { + path: PathBuf::from(artifact.filenames[0].to_string()), + manifest_path: PathBuf::from( + artifact.manifest_path.to_string(), + ), + }); + } + } + _ => {} + } + } + + // TODO: Fix. Not an ideal way to get the artifact of the desired crate, but it + // works for most case. + if let Some(artifact) = artifacts.pop() { + return Ok(artifact); + } + + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "failed to parse cargo output", + ))? + } else { + println!( + "failed to execute `cargo`: exited with {}\n full command: {:?}", + status, cmd, + ); + + std::process::exit(1); + } + } +} + +pub fn metadata() -> Result { + let metadata = cargo_metadata::MetadataCommand::new() + .exec() + .map_err(|e| { + println!("failed to execute `cargo metadata`: {}", e); + std::process::exit(1); + }) + .unwrap(); + + Ok(metadata.root_package().unwrap().name.clone()) +} diff --git a/deno_bindgen_cli/dlfcn.rs b/deno_bindgen_cli/dlfcn.rs new file mode 100644 index 0000000..5d0621a --- /dev/null +++ b/deno_bindgen_cli/dlfcn.rs @@ -0,0 +1,32 @@ +use std::path::Path; +use std::path::PathBuf; + +use dlopen2::wrapper::Container; +use dlopen2::wrapper::WrapperApi; + +#[derive(WrapperApi)] +struct Api { + init_deno_bindgen: unsafe fn(opt: deno_bindgen_ir::codegen::Options), +} + +pub unsafe fn load_and_init( + path: &Path, + out: Option, + lazy_init: bool, +) -> std::io::Result<()> { + let cont: Container = Container::load(path).map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::Other, + format!("failed to load library: {}", e), + ) + })?; + + cont.init_deno_bindgen(deno_bindgen_ir::codegen::Options { + target: deno_bindgen_ir::codegen::Target::Deno, + out, + local_dylib_path: path.to_path_buf(), + lazy_init, + }); + + Ok(()) +} diff --git a/deno_bindgen_cli/main.rs b/deno_bindgen_cli/main.rs new file mode 100644 index 0000000..808d762 --- /dev/null +++ b/deno_bindgen_cli/main.rs @@ -0,0 +1,39 @@ +use std::path::PathBuf; + +use cargo::Artifact; +use structopt::StructOpt; + +mod cargo; +mod dlfcn; + +#[derive(Debug, StructOpt)] +#[structopt(name = "deno_bindgen_cli", about = "A CLI for deno_bindgen")] +struct Opt { + #[structopt(short, long)] + /// Build in release mode + release: bool, + + #[structopt(short, long)] + out: Option, + + #[structopt(short, long)] + lazy_init: bool, +} + +fn main() -> std::io::Result<()> { + let opt = Opt::from_args(); + + let cwd = std::env::current_dir().unwrap(); + let Artifact { path, .. } = + cargo::Build::new().release(opt.release).build(&cwd)?; + + let name = cargo::metadata()?; + println!("Initializing {name}"); + + unsafe { + dlfcn::load_and_init(&PathBuf::from(path), opt.out, opt.lazy_init)? + }; + + println!("Ready {name}"); + Ok(()) +} diff --git a/deno_bindgen_ir/Cargo.toml b/deno_bindgen_ir/Cargo.toml new file mode 100644 index 0000000..f32a7e2 --- /dev/null +++ b/deno_bindgen_ir/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "deno_bindgen_ir" +version = "0.1.0" +description = "This tool aims to simplify glue code generation for Deno FFI libraries written in Rust." +documentation = "https://docs.rs/deno_bindgen" +homepage = "https://github.com/denoland/deno_bindgen" +repository = "https://github.com/denoland/deno_bindgen" +keywords = ["deno", "ffi", "bindgen", "bindings", "macro"] +categories = ["development-tools::ffi", "development-tools"] +readme = "../README.md" +license = "MIT" +edition = "2021" + +[lib] +path = "./lib.rs" + +[dependencies] +quote = "1.0" +proc-macro2 = "1.0" +syn = { version = "2.0", features = ["full", "extra-traits"] } \ No newline at end of file diff --git a/deno_bindgen_ir/codegen/deno.rs b/deno_bindgen_ir/codegen/deno.rs new file mode 100644 index 0000000..c3aa431 --- /dev/null +++ b/deno_bindgen_ir/codegen/deno.rs @@ -0,0 +1,429 @@ +use std::borrow::Cow; +use std::io::Result; +use std::io::Write; +use std::path::Path; + +use super::Generator; +use crate::inventory::Inventory; +use crate::inventory::Struct; +use crate::Type; + +// (ident, is_custom_type) +struct TypeScriptType<'a>(&'a str, bool); + +impl std::fmt::Display for TypeScriptType<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0)?; + Ok(()) + } +} + +impl TypeScriptType<'_> { + fn into_raw<'a>(&self, ident: &'a str) -> Cow<'a, str> { + match self { + Self("Uint8Array", false) => { + Cow::Owned(format!("{ident},\n {ident}.byteLength")) + } + Self(_, true) => Cow::Owned(format!("{ident}.ptr")), + _ => Cow::Borrowed(ident), + } + } + + fn from_raw<'a>(&self, ident: &'a str) -> Option { + match self { + Self(ty_str, true) => Some(format!("{ty_str}.__constructor({ident})")), + _ => None, + } + } + + fn apply_promise(&self, non_blocking: bool) -> Cow<'_, str> { + if non_blocking { + Cow::Owned(format!("Promise<{}>", self.0)) + } else { + Cow::Borrowed(self.0) + } + } +} + +impl From for TypeScriptType<'_> { + fn from(value: Type) -> Self { + Self( + match value { + Type::Void => "void", + Type::Uint8 + | Type::Uint16 + | Type::Uint32 + | Type::Uint64 + | Type::Int8 + | Type::Int16 + | Type::Int32 + | Type::Int64 + | Type::Float32 + | Type::Float64 => "number", + Type::Pointer => "Deno.PointerObject | null", + Type::Buffer => "Uint8Array", + Type::CustomType(name) => name, + }, + matches!(value, Type::CustomType(_)), + ) + } +} + +struct DenoFfiType(String); + +impl std::fmt::Display for DenoFfiType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0)?; + Ok(()) + } +} + +impl From for DenoFfiType { + fn from(value: Type) -> Self { + let ty = match value { + Type::Void => "void", + Type::Uint8 => "u8", + Type::Uint16 => "u16", + Type::Uint32 => "u32", + Type::Uint64 => "u64", + Type::Int8 => "i8", + Type::Int16 => "i16", + Type::Int32 => "i32", + Type::Int64 => "i64", + Type::Float32 => "f32", + Type::Float64 => "f64", + Type::CustomType(..) | Type::Pointer => "pointer", + Type::Buffer => "buffer", + }; + + let mut raw = format!("'{}'", ty); + + // Complex types. + if value == Type::Buffer { + raw.push_str(",\n 'usize'"); + } + + Self(raw) + } +} + +pub struct Codegen<'a> { + symbols: &'a [Inventory], + target: &'a Path, + lazy: bool, +} + +impl<'a> Codegen<'a> { + pub fn new(symbols: &'a [Inventory], target: &'a Path, lazy: bool) -> Self { + Self { + symbols, + target, + lazy, + } + } + + fn dlopen(&self, writer: &mut W) -> Result<()> { + if self.lazy { + return self.lazy_dlopen(writer); + } + writeln!(writer, "const {{ dlopen }} = Deno;\n")?; + let target = self.target.to_string_lossy(); + writeln!(writer, "const {{ symbols }} = dlopen('{target}', {{")?; + self.write_symbols(writer)?; + writeln!(writer, "}});\n")?; + + Ok(()) + } + + fn lazy_dlopen(&self, writer: &mut W) -> Result<()> { + writeln!(writer, "let symbols: any;\n")?; + let target = self.target.to_string_lossy(); + writeln!(writer, "export function load(path: string = '{target}') {{")?; + writeln!(writer, " const {{ dlopen }} = Deno;\n")?; + writeln!(writer, " const {{ symbols: symbols_ }} = dlopen(path, {{")?; + struct WrapperWriter<'a, W: Write> { + writer: &'a mut W, + indent: usize, + } + impl Write for WrapperWriter<'_, W> { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + // Find newlines and indent them. + for byte in buf { + if *byte == b'\n' { + self.writer.write_all(b"\n")?; + self.writer.write_all(&vec![b' '; self.indent])?; + } else { + self.writer.write_all(&[*byte])?; + } + } + + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.writer.flush() + } + } + write!(writer, " ")?; + let mut wr = WrapperWriter { writer, indent: 2 }; + self.write_symbols(&mut wr)?; + writeln!(wr, "}});\n")?; + write!(wr, "symbols = symbols_;")?; + writeln!(writer, "\n}}\n")?; + + Ok(()) + } + + fn write_symbols(&self, writer: &mut W) -> Result<()> { + fn format_bracket( + writer: &mut W, + items: &[T], + callback: impl Fn(&mut W, &[T]) -> Result<()>, + ) -> Result<()> { + write!(writer, "[")?; + if items.len() > 0 { + write!(writer, "\n")?; + callback(writer, items)?; + writeln!(writer, " ],")?; + } else { + writeln!(writer, "],")?; + } + + Ok(()) + } + + for symbol in self.symbols { + match symbol { + Inventory::Symbol(symbol) => { + writeln!(writer, " {}: {{", symbol.name)?; + write!(writer, " parameters: ")?; + format_bracket(writer, symbol.parameters, |writer, parameters| { + for parameter in parameters { + writeln!(writer, " {},", DenoFfiType::from(*parameter))?; + } + Ok(()) + })?; + writeln!( + writer, + " result: {},", + DenoFfiType::from(symbol.return_type) + )?; + writeln!(writer, " nonblocking: {}", symbol.non_blocking)?; + writeln!(writer, " }},")?; + } + _ => {} + } + } + + Ok(()) + } + + fn exports(&self, writer: &mut W) -> Result<()> { + fn format_paren( + writer: &mut W, + items: &[T], + allow_empty: bool, + callback: impl Fn(&mut W, &[T]) -> Result<()>, + nesting_spaces: usize, + delim: (char, &str), + ) -> Result<()> { + let (start, end) = delim; + write!(writer, "{start}")?; + if items.len() > 0 || allow_empty { + write!(writer, "\n")?; + callback(writer, items)?; + write!(writer, "{:indent$}{end}", "", indent = nesting_spaces)?; + } else { + write!(writer, "{end}")?; + } + + Ok(()) + } + + for symbol in self.symbols { + match symbol { + Inventory::Symbol(symbol) => { + if !symbol.internal { + write!(writer, "export ")?; + } + write!(writer, "function {}", symbol.name)?; + format_paren( + writer, + symbol.parameters, + false, + |writer, parameters| { + for (i, parameter) in parameters.iter().enumerate() { + writeln!( + writer, + " arg{}: {},", + i, + TypeScriptType::from(*parameter) + )?; + } + Ok(()) + }, + 0, + ('(', ")"), + )?; + let ret_ty = TypeScriptType::from(symbol.return_type); + writeln!( + writer, + ": {} {{", + ret_ty.apply_promise(symbol.non_blocking) + )?; + let maybe_ret_transform = ret_ty.from_raw("ret"); + if maybe_ret_transform.is_some() { + write!(writer, " const ret = ")?; + } else { + write!(writer, " return ")?; + } + write!(writer, "symbols.{}", symbol.name)?; + format_paren( + writer, + symbol.parameters, + false, + |writer, parameters| { + for (i, parameter) in parameters.iter().enumerate() { + let ident = format!("arg{}", i); + writeln!( + writer, + " {},", + TypeScriptType::from(*parameter).into_raw(&ident) + )?; + } + Ok(()) + }, + 2, + ('(', ")"), + )?; + + if let Some(ret_transform) = maybe_ret_transform { + write!(writer, "\n return {ret_transform};")?; + } + writeln!(writer, "\n}}\n")?; + } + Inventory::Struct(Struct { name, methods }) => { + write!(writer, "export class {name} ")?; + + format_paren( + writer, + &methods, + false, + |writer, methods| { + writeln!(writer, " ptr: Deno.PointerObject | null = null;\n")?; + + // Internal constructor. + writeln!( + writer, + " static __constructor(ptr: Deno.PointerObject | null) {{" + )?; + writeln!( + writer, + " const self = Object.create({name}.prototype);" + )?; + writeln!(writer, " self.ptr = ptr;")?; + writeln!(writer, " return self;")?; + writeln!(writer, " }}\n")?; + + // Dispose method (explicit resource management) + writeln!(writer, " [Symbol.dispose]() {{")?; + writeln!(writer, " this.dealloc();")?; + writeln!(writer, " this.ptr = null;")?; + writeln!(writer, " }}")?; + + for method in methods { + let mut parameters = method.parameters; + + if !method.is_constructor { + // Skip the self ptr argument. + parameters = &method.parameters[1..]; + } + + let method_name = if method.is_constructor { + "constructor" + } else { + &method.name + }; + + write!( + writer, + "\n {name}({parameters})", + name = method_name, + parameters = parameters + .iter() + .enumerate() + .map(|(i, parameter)| { + format!("arg{}: {}", i, TypeScriptType::from(*parameter)) + }) + .collect::>() + .join(", "), + )?; + + if !method.is_constructor { + let return_type = TypeScriptType::from(method.return_type); + writeln!(writer, ": {return_type} {{")?; + } else { + // Typescript doesn't allow constructors to have a return type. + writeln!(writer, " {{")?; + } + + // Apply name mangling. + write!(writer, " return __{}_{}", name, method.name)?; + format_paren( + writer, + parameters, + !method.is_constructor, + |writer, parameters| { + if !method.is_constructor { + writeln!(writer, " this.ptr,",)?; + } + + for (i, parameter) in parameters.iter().enumerate() { + let ident = format!("arg{}", i); + writeln!( + writer, + " {},", + TypeScriptType::from(*parameter).into_raw(&ident) + )?; + } + + Ok(()) + }, + 4, + ('(', ")"), + )?; + + writeln!(writer, "\n }}")?; + } + Ok(()) + }, + 0, + ('{', "}\n\n"), + )?; + } + } + } + + Ok(()) + } +} + +impl<'a> Generator for Codegen<'a> { + fn generate(&mut self, mut writer: W) -> Result<()> { + fn prelude(writer: &mut W) -> Result<()> { + writeln!(writer, "// deno-lint-ignore-file\n")?; + writeln!( + writer, + "// This file is automatically generated by deno_bindgen." + )?; + writeln!(writer, "// Do not edit this file directly.\n")?; + Ok(()) + } + + prelude(&mut writer)?; + self.dlopen(&mut writer)?; + self.exports(&mut writer)?; + + Ok(()) + } +} diff --git a/deno_bindgen_ir/codegen/mod.rs b/deno_bindgen_ir/codegen/mod.rs new file mode 100644 index 0000000..4211fbd --- /dev/null +++ b/deno_bindgen_ir/codegen/mod.rs @@ -0,0 +1,40 @@ +use std::path::PathBuf; + +use crate::inventory::Inventory; + +mod deno; + +pub struct Options { + pub target: Target, + pub out: Option, + pub local_dylib_path: PathBuf, + pub lazy_init: bool, +} + +pub enum Target { + Deno, +} + +pub trait Generator { + fn generate(&mut self, writer: W) -> std::io::Result<()>; +} + +pub fn generate( + symbols: &'static [Inventory], + opt: Options, +) -> std::io::Result<()> { + let mut codegen = match opt.target { + Target::Deno => { + deno::Codegen::new(symbols, &opt.local_dylib_path, opt.lazy_init) + } + }; + + if let Some(out) = opt.out { + let mut writer = std::fs::File::create(out)?; + codegen.generate(&mut writer)?; + return Ok(()); + } + + let writer = std::io::stdout(); + codegen.generate(writer) +} diff --git a/deno_bindgen_ir/inventory.rs b/deno_bindgen_ir/inventory.rs new file mode 100644 index 0000000..5a6cf96 --- /dev/null +++ b/deno_bindgen_ir/inventory.rs @@ -0,0 +1,12 @@ +use crate::Symbol; + +#[derive(Debug)] +pub struct Struct { + pub name: &'static str, + pub methods: &'static [Symbol], +} + +pub enum Inventory { + Symbol(Symbol), + Struct(Struct), +} diff --git a/deno_bindgen_ir/lib.rs b/deno_bindgen_ir/lib.rs new file mode 100644 index 0000000..1cf7312 --- /dev/null +++ b/deno_bindgen_ir/lib.rs @@ -0,0 +1,220 @@ +use proc_macro2::Ident; +use quote::quote; +use quote::ToTokens; +use syn::parse_quote; +use syn::Pat; + +pub mod codegen; +pub mod inventory; + +pub use inventory::Inventory; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)] +pub enum Type { + #[default] + Void, + Uint8, + Uint16, + Uint32, + Uint64, + Int8, + Int16, + Int32, + Int64, + Float32, + Float64, + Pointer, + Buffer, + + CustomType(&'static str), +} + +pub type RawTypes = &'static [Type]; + +impl Type { + pub fn raw(&self) -> RawTypes { + match self { + Self::Buffer => &[Self::Pointer, Self::Uint32], + Self::Pointer | Self::CustomType(..) => &[Self::Pointer], + _ => &[], + } + } + + pub fn is_number(&self) -> bool { + !matches!( + self, + Self::Void | Self::Pointer | Self::Buffer | Self::CustomType(_) + ) + } + + pub fn apply_arg_transform( + &self, + name: &mut Box, + args: &[Ident], + ) -> Option { + match self { + Self::Buffer => { + let pointer = &args[0]; + let length = &args[1]; + Some(quote! { + let #name = unsafe { + std::slice::from_raw_parts_mut(#pointer as _, #length as usize) + }; + }) + } + Self::CustomType(_) => { + let pointer = &args[0]; + Some(quote! { + debug_assert!(!#pointer.is_null()); + let #name = unsafe { &mut *(#pointer as *mut _) }; + }) + } + Self::Pointer => { + let pointer = &args[0]; + Some(quote! { + let #name = #pointer as _; + }) + } + _ => None, + } + } + + pub fn apply_ret_transform( + &self, + name: &mut Box, + arg: Ident, + ) -> Option { + match self { + Self::Pointer => Some(quote! { + let #name = #arg as _; + }), + Self::CustomType(_) => Some(quote! { + let #name = Box::into_raw(Box::new(#arg)) as *mut _; + }), + _ => None, + } + } + + pub fn to_ident(&self) -> syn::Expr { + match self { + Self::Void => parse_quote!(deno_bindgen::Type::Void), + Self::Uint8 => parse_quote!(deno_bindgen::Type::Uint8), + Self::Uint16 => parse_quote!(deno_bindgen::Type::Uint16), + Self::Uint32 => parse_quote!(deno_bindgen::Type::Uint32), + Self::Uint64 => parse_quote!(deno_bindgen::Type::Uint64), + Self::Int8 => parse_quote!(deno_bindgen::Type::Int8), + Self::Int16 => parse_quote!(deno_bindgen::Type::Int16), + Self::Int32 => parse_quote!(deno_bindgen::Type::Int32), + Self::Int64 => parse_quote!(deno_bindgen::Type::Int64), + Self::Float32 => parse_quote!(deno_bindgen::Type::Float32), + Self::Float64 => parse_quote!(deno_bindgen::Type::Float64), + Self::Pointer => parse_quote!(deno_bindgen::Type::Pointer), + Self::Buffer => parse_quote!(deno_bindgen::Type::Buffer), + Self::CustomType(s) => parse_quote!(deno_bindgen::Type::CustomType(#s)), + } + } +} + +impl ToTokens for Type { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let ty = match self { + Self::Void => quote! { () }, + Self::Uint8 => quote! { u8 }, + Self::Uint16 => quote! { u16 }, + Self::Uint32 => quote! { u32 }, + Self::Uint64 => quote! { u64 }, + Self::Int8 => quote! { i8 }, + Self::Int16 => quote! { i16 }, + Self::Int32 => quote! { i32 }, + Self::Int64 => quote! { i64 }, + Self::Float32 => quote! { f32 }, + Self::Float64 => quote! { f64 }, + Self::CustomType(_) | Self::Pointer => quote! { *const () }, + Self::Buffer => quote! { *mut u8 }, + }; + + tokens.extend(ty); + } +} + +#[derive(Debug)] +pub struct Symbol { + pub name: &'static str, + pub parameters: &'static [Type], + pub return_type: Type, + pub non_blocking: bool, + pub internal: bool, + pub is_constructor: bool, +} + +pub struct SymbolBuilder { + name: Ident, + parameters: Vec, + return_type: Type, + non_blocking: bool, + internal: bool, + is_constructor: bool, +} + +impl SymbolBuilder { + pub fn new(name: Ident) -> Self { + Self { + name, + parameters: Vec::new(), + return_type: Default::default(), + non_blocking: false, + internal: false, + is_constructor: false, + } + } + + pub fn set_name(&mut self, name: Ident) { + self.name = name; + } + + pub fn push(&mut self, ty: Type) { + self.parameters.push(ty); + } + + pub fn return_type(&mut self, ty: Type) { + self.return_type = ty; + } + + pub fn non_blocking(&mut self, non_blocking: bool) { + self.non_blocking = non_blocking; + } + + pub fn internal(&mut self, internal: bool) { + self.internal = internal; + } + + pub fn is_constructor(&mut self, is_constructor: bool) { + self.is_constructor = is_constructor; + } +} + +impl ToTokens for SymbolBuilder { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let parameters = &self + .parameters + .iter() + .map(|ty| ty.to_ident()) + .collect::>(); + let return_type = &self.return_type.to_ident(); + let non_blocking = &self.non_blocking; + let name = &self.name; + let internal = &self.internal; + let is_constructor = &self.is_constructor; + + tokens.extend(quote! { + deno_bindgen::Symbol { + name: stringify!(#name), + parameters: &[#(#parameters),*], + return_type: #return_type, + non_blocking: #non_blocking, + internal: #internal, + is_constructor: #is_constructor, + } + }); + } +} diff --git a/deno_bindgen_macro/Cargo.toml b/deno_bindgen_macro/Cargo.toml index 04d464e..736f529 100644 --- a/deno_bindgen_macro/Cargo.toml +++ b/deno_bindgen_macro/Cargo.toml @@ -15,9 +15,14 @@ edition = "2021" proc-macro = true [dependencies] -proc-macro2 = "1" -quote = "1" -syn = { version = "1", features = ["full", "extra-traits"] } +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "2.0", features = ["full", "extra-traits"] } serde = { version = "1.0.59", features = ["derive"] } serde_json = "1.0.59" Inflector = "0.11.4" +deno_bindgen_ir = { path = "../deno_bindgen_ir", version = "0.1.0" } + +[dev-dependencies] +prettyplease = "0.2.15" +testing_macros = "0.2.11" \ No newline at end of file diff --git a/deno_bindgen_macro/src/attrs.rs b/deno_bindgen_macro/src/attrs.rs deleted file mode 100644 index ba30b5a..0000000 --- a/deno_bindgen_macro/src/attrs.rs +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright 2020-2021 the Deno authors. All rights reserved. MIT license. - -use syn::Attribute; -use syn::Lit; -use syn::Meta; -use syn::NestedMeta; - -use inflector::Inflector; - -#[derive(Debug)] -pub enum SerdeAttr { - RenameAll(String), - TagAndContent(String, String), -} - -impl SerdeAttr { - pub fn transform(&self, s: &str) -> Option { - match self { - SerdeAttr::RenameAll(t) => match t.as_ref() { - "lowercase" => Some(s.to_lowercase()), - "UPPERCASE" => Some(s.to_uppercase()), - "camelCase" => Some(s.to_camel_case()), - "snake_case" => Some(s.to_snake_case()), - "PascalCase" => Some(s.to_pascal_case()), - "SCREAMING_SNAKE_CASE" => Some(s.to_screaming_snake_case()), - _ => panic!("Invalid attribute value: {}", s), - }, - _ => None, - } - } -} - -pub fn get_serde_attrs(attrs: &Vec) -> Vec { - attrs - .iter() - .filter(|i| i.path.is_ident("serde")) - .flat_map(|attr| match attr.parse_meta() { - Ok(Meta::List(l)) => l.nested.iter().find_map(|meta| match meta { - NestedMeta::Meta(Meta::NameValue(v)) => match v.path.get_ident() { - Some(id) => match id.to_string().as_ref() { - // #[serde(rename_all = "UPPERCASE")] - "rename_all" => match &v.lit { - Lit::Str(s) => Some(SerdeAttr::RenameAll(s.value())), - _ => None, - }, - // #[serde(tag = "key", content = "value")] - "tag" => match &v.lit { - Lit::Str(s) => { - let tag = s.value(); - - let lit = l.nested.iter().find_map(|meta| match meta { - NestedMeta::Meta(Meta::NameValue(v)) => { - match v.path.is_ident("content") { - true => Some(&v.lit), - false => None, - } - } - _ => None, - }); - - match lit { - Some(Lit::Str(s)) => { - let content = s.value(); - Some(SerdeAttr::TagAndContent(tag, content)) - } - _ => panic!("Missing `content` attribute with `tag`."), - } - } - _ => None, - }, - // #[serde(content = "value", tag = "key")] - "content" => match &v.lit { - Lit::Str(s) => { - let content = s.value(); - - let lit = l.nested.iter().find_map(|meta| match meta { - NestedMeta::Meta(Meta::NameValue(v)) => { - match v.path.is_ident("tag") { - true => Some(&v.lit), - false => None, - } - } - _ => None, - }); - - match lit { - Some(Lit::Str(s)) => { - let tag = s.value(); - Some(SerdeAttr::TagAndContent(tag, content)) - } - _ => panic!("Missing `tag` attribute with `content`."), - } - } - _ => None, - }, - _ => None, - }, - _ => None, - }, - _ => None, - }), - _ => None, - }) - .collect::>() -} diff --git a/deno_bindgen_macro/src/derive_fn.rs b/deno_bindgen_macro/src/derive_fn.rs deleted file mode 100644 index a06242c..0000000 --- a/deno_bindgen_macro/src/derive_fn.rs +++ /dev/null @@ -1,208 +0,0 @@ -// Copyright 2020-2021 the Deno authors. All rights reserved. MIT license. - -use crate::meta::Glue; -use crate::meta::Symbol; -use crate::meta::Type; -use syn::AttributeArgs; -use syn::FnArg; -use syn::ItemFn; -use syn::Meta; -use syn::NestedMeta; -use syn::PathArguments; -use syn::ReturnType; - -pub fn process_function( - function: ItemFn, - attr: AttributeArgs, - metadata: &mut Glue, -) -> Result { - let params = &function.sig.inputs; - let mut parameters = Vec::with_capacity(params.len()); - - for param in params.iter() { - match param { - FnArg::Typed(ref val) => { - let val = val.clone(); - let ty = match *val.ty { - syn::Type::Path(ref ty) => { - let segment = ty.path.segments.first().unwrap(); - let ident = segment.ident.to_string(); - match ident.as_str() { - "i8" => Type::I8, - "u8" => Type::U8, - "i16" => Type::I16, - "u16" => Type::U16, - "i32" => Type::I32, - "u32" => Type::U32, - "i64" => Type::I64, - "u64" => Type::U64, - "usize" => Type::Usize, - "isize" => Type::Isize, - "f32" => Type::F32, - "f64" => Type::F64, - _ => { - metadata.type_defs.get(&ident).expect(&format!( - "Type definition not found for `{}` identifier", - &ident, - )); - - Type::StructEnum { ident } - } - } - } - syn::Type::Reference(ref ty) => match *ty.elem { - syn::Type::Path(ref ty) => { - let segment = ty.path.segments.first().unwrap(); - let ident = segment.ident.to_string(); - - match ident.as_str() { - "str" => Type::Str, - _ => unimplemented!(), - } - } - syn::Type::Slice(ref slice) => match *slice.elem { - syn::Type::Path(ref path) => { - let segment = path.path.segments.first().unwrap(); - let ident = segment.ident.to_string(); - - match ident.as_str() { - "u8" => { - if ty.mutability.is_some() { - Type::BufferMut - } else { - Type::Buffer - } - } - _ => unimplemented!(), - } - } - _ => unimplemented!(), - }, - _ => unimplemented!(), - }, - _ => unimplemented!(), - }; - - parameters.push(ty); - } - _ => unimplemented!(), - } - } - - let result = match &function.sig.output { - ReturnType::Default => Type::Void, - ReturnType::Type(_, ref ty) => match ty.as_ref() { - syn::Type::Ptr(_) => Type::Ptr, - syn::Type::Path(ref ty) => { - let segment = ty.path.segments.first().unwrap(); - let ident = segment.ident.to_string(); - - match ident.as_str() { - "i8" => Type::I8, - "u8" => Type::U8, - "i16" => Type::I16, - "u16" => Type::U16, - "i32" => Type::I32, - "u32" => Type::U32, - "i64" => Type::I64, - "u64" => Type::U64, - "usize" => Type::Usize, - "isize" => Type::Isize, - "f32" => Type::F32, - "f64" => Type::F64, - // This isn't a &str but i really but - // don't want to add another type for just owned strings. - "String" => Type::Str, - "Box" => { - let mut buf = false; - let mut gn = String::default(); - if let PathArguments::AngleBracketed(args) = &segment.arguments { - if let Some(syn::GenericArgument::Type(syn::Type::Slice(args))) = - &args.args.first() - { - if let syn::Type::Path(args) = &*args.elem { - if let Some(args) = args.path.segments.first() { - gn = args.ident.to_string(); - if gn == "u8" { - buf = true; - } - } - } - } - } - if buf { - Type::Buffer - } else { - panic!("{}<{}> return type not supported by Deno FFI", ident, gn) - } - } - "Vec" => { - let mut buf = false; - let mut gn = String::default(); - if let PathArguments::AngleBracketed(args) = &segment.arguments { - if let Some(syn::GenericArgument::Type(syn::Type::Path(args))) = - &args.args.first() - { - if let Some(args) = &args.path.segments.first() { - gn = args.ident.to_string(); - if gn == "u8" { - buf = true; - } - } - } - } - if buf { - Type::Buffer - } else { - panic!("{}<{}> return type not supported by Deno FFI", ident, gn) - } - } - _ => match metadata.type_defs.get(&ident) { - Some(_) => Type::StructEnum { ident }, - None => panic!("{} return type not supported by Deno FFI", ident), - }, - } - } - syn::Type::Reference(ref ty) => match *ty.elem { - syn::Type::Slice(ref slice) => match *slice.elem { - syn::Type::Path(ref path) => { - let segment = path.path.segments.first().unwrap(); - let ident = segment.ident.to_string(); - - match ident.as_str() { - "u8" => { - if ty.mutability.is_some() { - // https://github.com/denoland/deno_bindgen/issues/39 - panic!("Mutable slices are not mutable from JS"); - } else { - Type::Buffer - } - } - _ => unimplemented!(), - } - } - _ => unimplemented!(), - }, - _ => unimplemented!(), - }, - _ => unimplemented!(), - }, - }; - - let symbol_name = function.sig.ident.to_string(); - let non_blocking = match attr.get(0).as_ref() { - Some(NestedMeta::Meta(Meta::Path(ref attr_ident))) => { - attr_ident.is_ident("non_blocking") - } - _ => false, - }; - - let symbol = Symbol { - parameters, - result, - non_blocking, - }; - metadata.symbols.insert(symbol_name, symbol.clone()); - - Ok(symbol) -} diff --git a/deno_bindgen_macro/src/derive_struct.rs b/deno_bindgen_macro/src/derive_struct.rs deleted file mode 100644 index bba442b..0000000 --- a/deno_bindgen_macro/src/derive_struct.rs +++ /dev/null @@ -1,274 +0,0 @@ -// Copyright 2020-2021 the Deno authors. All rights reserved. MIT license. - -use crate::attrs::get_serde_attrs; -use crate::attrs::SerdeAttr; -use crate::docs::get_docs; -use crate::meta::Glue; - -use std::collections::HashMap; -use syn::ext::IdentExt; -use syn::Data; -use syn::DataStruct; -use syn::DeriveInput; -use syn::Fields; - -macro_rules! variant_instance { - ( $variant:path, $iterator:expr ) => { - $iterator - .filter_map(|val| { - if let $variant(ref f1, ref f2) = *val { - Some((f1, f2)) - } else { - None - } - }) - .next() - }; -} - -pub fn process_struct( - metadata: &mut Glue, - input: DeriveInput, -) -> Result<(), String> { - match &input.data { - Data::Struct(DataStruct { - fields: Fields::Named(fields), - .. - }) => { - let fields = &fields.named; - - let name = &input.ident; - let mut fmap = HashMap::new(); - let mut typescript: Vec = vec![]; - - let serde_attrs = get_serde_attrs(&input.attrs); - - for field in fields.iter() { - let mut ident = field - .ident - .as_ref() - .expect("Field without ident") - // Strips the raw marker `r#`, if present. - .unraw() - .to_string(); - - match field.ty { - syn::Type::Path(ref ty) => { - let segment = &ty.path.segments.first().unwrap(); - let ty = segment.ident.to_string(); - fmap.insert(ident.clone(), ty); - } - syn::Type::Reference(ref ty) => { - assert!(!ty.mutability.is_some()); - assert!(ty.lifetime.is_some()); - match *ty.elem { - syn::Type::Path(ref ty) => { - let segment = &ty.path.segments.first().unwrap(); - let ty = segment.ident.to_string(); - fmap.insert(ident.clone(), ty); - } - _ => unimplemented!(), - } - } - _ => unimplemented!(), - }; - - for attr in &serde_attrs { - if let Some(i) = attr.transform(&ident) { - ident = i; - } - } - - let doc_str = get_docs(&field.attrs); - typescript.push(format!( - "{} {}: {};", - doc_str, - ident, - types_to_ts(&field.ty) - )); - } - - metadata.type_defs.insert(name.to_string(), fmap.clone()); - - let doc_str = get_docs(&input.attrs); - let typescript = format!( - "{}export type {} = {{\n {}\n}};", - doc_str, - name, - typescript.join("\n") - ); - metadata.ts_types.insert(name.to_string(), typescript); - Ok(()) - } - Data::Enum(syn::DataEnum { variants, .. }) => { - let name = &input.ident; - let mut typescript: Vec = vec![]; - - for variant in variants { - let mut variant_fields: Vec = vec![]; - let fields = &variant.fields; - - let serde_attrs = get_serde_attrs(&input.attrs); - for field in fields { - let mut ident = field - .ident - .as_ref() - .expect("Field without ident") - // Strips the raw marker `r#`, if present. - .unraw() - .to_string(); - - for attr in &serde_attrs { - if let Some(i) = attr.transform(&ident) { - ident = i; - } - } - - let doc_str = get_docs(&field.attrs); - variant_fields.push(format!( - "{} {}: {};", - doc_str, - ident, - types_to_ts(&field.ty) - )); - } - - let mut ident = variant.ident.to_string(); - - // Perform #[serde] attribute transformers. - // This excludes `tag` and `content` attributes. - // They require special treatment during codegen. - for attr in &serde_attrs { - if let Some(i) = attr.transform(&ident) { - ident = i; - } - } - - let doc_str = get_docs(&variant.attrs); - - let variant_str = if variant_fields.len() > 0 { - let tag_content = - variant_instance!(SerdeAttr::TagAndContent, serde_attrs.iter()); - - match tag_content { - None => { - format!( - "{} {{ {}: {{\n {}\n}} }}", - doc_str, - &ident, - variant_fields.join("\n") - ) - } - Some((tag, content)) => { - // // $jsdoc - // { - // $tag: $ident, - // $content: { ...$fields } - // } - format!( - "{} {{ {}: \"{}\", {}: {{ {} }} }}", - doc_str, - &tag, - &ident, - &content, - variant_fields.join("\n") - ) - } - } - } else { - format!("{} \"{}\"", doc_str, &ident) - }; - - typescript.push(variant_str); - } - - // TODO: `type_defs` in favor of `ts_types` - metadata.type_defs.insert(name.to_string(), HashMap::new()); - - let doc_str = get_docs(&input.attrs); - let typescript = format!( - "{}export type {} = {};", - doc_str, - name, - typescript.join(" |\n") - ); - metadata.ts_types.insert(name.to_string(), typescript); - Ok(()) - } - _ => unimplemented!(), - } -} - -fn types_to_ts(ty: &syn::Type) -> String { - match ty { - syn::Type::Array(_) => String::from("any"), - syn::Type::Ptr(_) => String::from("any"), - syn::Type::Reference(ref ty) => types_to_ts(&ty.elem), - syn::Type::Path(ref ty) => { - // std::alloc::Vec => Vec - let segment = &ty.path.segments.last().unwrap(); - let ty = segment.ident.to_string(); - let mut generics: Vec = vec![]; - let generic_params = &segment.arguments; - match generic_params { - &syn::PathArguments::AngleBracketed(ref args) => { - for p in &args.args { - let ty = match p { - syn::GenericArgument::Type(ty) => types_to_ts(ty), - syn::GenericArgument::Lifetime(_) => continue, - _ => unimplemented!(), - }; - generics.push(ty); - } - } - &syn::PathArguments::None => {} - _ => unimplemented!(), - }; - - match ty.as_ref() { - "Option" => format!( - "{} | undefined | null", - rs_to_ts(generics.first().unwrap().as_ref()) - ), - _ => { - if generics.len() > 0 { - let root_ty = rs_to_ts(&ty); - let generic_str = generics - .iter() - .map(|g| rs_to_ts(g)) - .collect::>() - .join(", "); - format!("{}<{}>", root_ty, generic_str) - } else { - rs_to_ts(&ty).to_string() - } - } - } - } - _ => unimplemented!(), - } -} - -fn rs_to_ts(ty: &str) -> &str { - match ty { - "i8" => "number", - "i16" => "number", - "i32" => "number", - "i64" => "number", - "u8" => "number", - "u16" => "number", - "u32" => "number", - "u64" => "number", - "usize" => "number", - "bool" => "boolean", - "String" => "string", - "str" => "string", - "f32" => "number", - "f64" => "number", - "HashMap" => "Record", - "Vec" => "Array", - "HashSet" => "Array", - "Value" => "any", - a @ _ => a, - } -} diff --git a/deno_bindgen_macro/src/docs.rs b/deno_bindgen_macro/src/docs.rs deleted file mode 100644 index 4670cbb..0000000 --- a/deno_bindgen_macro/src/docs.rs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2020-2021 the Deno authors. All rights reserved. MIT license. - -use syn::Attribute; -use syn::Lit; -use syn::Meta; - -pub fn get_docs(attrs: &Vec) -> String { - let mut doc: Vec = vec![]; - for attr in attrs { - if let Ok(Meta::NameValue(meta)) = attr.parse_meta() { - if !meta.path.is_ident("doc") { - continue; - } - if let Lit::Str(lit) = meta.lit { - doc.push(lit.value()); - } - } - } - - let doc_str = if doc.len() > 0 { - format!("/**\n *{}\n **/\n", doc.join("\n *")) - } else { - String::new() - }; - - doc_str -} diff --git a/deno_bindgen_macro/src/fn_.rs b/deno_bindgen_macro/src/fn_.rs new file mode 100644 index 0000000..e320398 --- /dev/null +++ b/deno_bindgen_macro/src/fn_.rs @@ -0,0 +1,215 @@ +use deno_bindgen_ir::SymbolBuilder; +use deno_bindgen_ir::Type; +use proc_macro2::Ident; +use proc_macro2::Span; +use proc_macro2::TokenStream as TokenStream2; +use syn::parse_quote; +use syn::punctuated::Punctuated; +use syn::spanned::Spanned; +use syn::token::Comma; +use syn::FnArg; +use syn::ItemFn; +use syn::PatType; +use syn::ReturnType; +use syn::TypePath; +use syn::TypePtr; +use syn::TypeReference; +use syn::TypeSlice; + +use crate::util::Error; +use crate::util::Result; +use crate::FnAttributes; + +fn custom_type(ty: &str) -> Type { + // yeah, don't worry about it. + Type::CustomType(Box::leak(ty.to_string().into_boxed_str())) +} + +fn parse_type(ty: &Box) -> Result { + match **ty { + syn::Type::Path(TypePath { ref path, .. }) => { + if let Some(ident) = path.get_ident() { + match ident.to_string().as_str() { + "u8" => return Ok(Type::Uint8), + "u16" => return Ok(Type::Uint16), + "u32" => return Ok(Type::Uint32), + "u64" => return Ok(Type::Uint64), + "i8" => return Ok(Type::Int8), + "i16" => return Ok(Type::Int16), + "i32" => return Ok(Type::Int32), + "i64" => return Ok(Type::Int64), + "f32" => return Ok(Type::Float32), + "f64" => return Ok(Type::Float64), + "usize" => return Ok(Type::Uint64), + "isize" => return Ok(Type::Int64), + ty_str => { + return Ok(custom_type(ty_str)); + } + } + } + + Err(Error::UnsupportedType) + } + syn::Type::Reference(TypeReference { ref elem, .. }) => { + if let syn::Type::Slice(TypeSlice { ref elem, .. }) = *elem.as_ref() { + if parse_type(elem)?.is_number() { + return Ok(Type::Buffer); + } + } + + if let syn::Type::Path(TypePath { ref path, .. }) = *elem.as_ref() { + if let Some(ident) = path.get_ident() { + let ref ty_str = ident.to_string(); + return Ok(custom_type(ty_str)); + } + } + + Err(Error::UnsupportedType) + } + + syn::Type::Ptr(TypePtr { .. }) => Ok(Type::Pointer), + _ => Err(Error::UnsupportedType), + } +} + +pub fn handle_inner( + fn_: ItemFn, + attrs: FnAttributes, +) -> Result<(TokenStream2, SymbolBuilder)> { + if fn_.sig.asyncness.is_some() { + return Err(Error::Asyncness); + } + + if fn_.sig.receiver().is_some() { + return Err(Error::Reciever); + } + + // TODO: check right ABI + + let mut ffi_fn = fn_.clone(); + ffi_fn.sig.abi.get_or_insert_with(|| { + parse_quote!( + extern "C" + ) + }); + + let mut inputs: Punctuated = Punctuated::new(); + let mut transforms: Vec = Vec::new(); + + let mut symbol = SymbolBuilder::new(fn_.sig.ident.clone()); + symbol.non_blocking(attrs.non_blocking); + symbol.internal(attrs.internal); + symbol.is_constructor(attrs.constructor); + + // Cannot use enumerate here, there can be multiple raw args per type. + let mut i = 0; + for arg in ffi_fn.sig.inputs.iter_mut() { + match *arg { + FnArg::Receiver(_) => unreachable!(), + FnArg::Typed(PatType { + ref mut pat, + ref mut ty, + .. + }) => { + let ty = parse_type(ty)?; + symbol.push(ty); + + const X_ARG_PREFIX: &str = "__arg_"; + // Divide the type into its raw components. + let raw_ty = ty.raw(); + + // Complex types, that need transforms. + let mut idents = Vec::new(); + for ty in raw_ty { + let arg_name = Ident::new( + &format!("{}{}", X_ARG_PREFIX, i), + Span::mixed_site().located_at(pat.span()), + ); + inputs.push(parse_quote!(#arg_name: #ty)); + idents.push(arg_name); + i += 1; + } + + // Apply the transform. + if let Some(transform) = ty.apply_arg_transform(pat, &idents) { + transforms.push(transform); + } + + // Simple type. + if raw_ty.len() == 0 { + inputs.push(arg.clone()); + i += 1; + } + } + } + } + + let ret_ident = Ident::new("ret", Span::mixed_site()); + let mut ret = Box::new(syn::Pat::Ident(syn::PatIdent { + attrs: Vec::new(), + by_ref: None, + mutability: None, + ident: ret_ident.clone(), + subpat: None, + })); + let mut ret_transform = TokenStream2::new(); + match ffi_fn.sig.output { + ReturnType::Default => {} + ReturnType::Type(_, ref mut ty) => { + let t = parse_type(ty)?; + + if let Some(transform) = + t.apply_ret_transform(&mut ret, ret_ident.clone()) + { + ret_transform = transform; + } + + symbol.return_type(t); + **ty = parse_quote!(#t) + } + } + + let idents = ffi_fn + .sig + .inputs + .iter() + .map(|arg| match arg { + FnArg::Receiver(_) => unreachable!(), + FnArg::Typed(PatType { ref pat, .. }) => match &**pat { + syn::Pat::Ident(ident) => ident.ident.clone(), + _ => unreachable!(), + }, + }) + .collect::>(); + + let name = fn_.sig.ident.clone(); + ffi_fn.sig.inputs = inputs; + + ffi_fn.block = parse_quote!({ + #fn_ + + #(#transforms)* + + let #ret_ident = #name(#(#idents),*); + #ret_transform + #ret_ident + }); + + Ok(( + quote::quote! { + const _: () = { + #[deno_bindgen::linkme::distributed_slice(deno_bindgen::INVENTORY)] + pub static _A: deno_bindgen::Inventory = deno_bindgen::Inventory::Symbol(#symbol); + }; + + #[no_mangle] + #ffi_fn + }, + symbol, + )) +} + +pub fn handle(fn_: ItemFn, attrs: FnAttributes) -> Result { + let (ffi_fn, _) = handle_inner(fn_, attrs)?; + Ok(ffi_fn) +} diff --git a/deno_bindgen_macro/src/impl_.rs b/deno_bindgen_macro/src/impl_.rs new file mode 100644 index 0000000..dcd6a8e --- /dev/null +++ b/deno_bindgen_macro/src/impl_.rs @@ -0,0 +1,150 @@ +use proc_macro2::TokenStream as TokenStream2; +use quote::format_ident; +use syn::parse_quote; +use syn::punctuated::Punctuated; +use syn::ImplItemFn; +use syn::ItemImpl; + +use crate::util::Result; +use crate::util::{self}; + +pub fn handle(mut impl_: ItemImpl) -> Result { + if impl_.generics.params.first().is_some() { + return Err(util::Error::Generics); + } + + if impl_.generics.where_clause.is_some() { + return Err(util::Error::WhereClause); + } + + let self_ty = match *impl_.self_ty { + syn::Type::Path(ref type_path) => type_path.path.clone(), + _ => return Err(util::Error::UnsupportedType), + }; + + let ref ty_str @ _ = self_ty.get_ident().unwrap(); + + let mut methods = Vec::new(); + let mut syms = Punctuated::::new(); + for item in impl_.items.iter_mut() { + match item { + syn::ImplItem::Fn(ImplItemFn { sig, attrs, .. }) => { + let mut is_constructor = false; + if let Some(attr) = attrs.first() { + let path = attr.path(); + is_constructor = path.is_ident("constructor"); + + attrs.clear(); + } + + // TODO: Add common name magling util. + let method_name = sig.ident.clone(); + let mangled_name = format_ident!("__{}_{}", ty_str, method_name); + // ... + let ref out = sig.output; + let inputs = sig.inputs.iter(); + + fn idents_with_skip<'a>( + arg: syn::punctuated::Iter<'a, syn::FnArg>, + skip: usize, + ) -> Vec<&'a syn::Ident> { + arg + .skip(skip) + .map(|arg| match arg { + syn::FnArg::Receiver(_) => unreachable!(), + syn::FnArg::Typed(pat_type) => match &*pat_type.pat { + syn::Pat::Ident(ident) => &ident.ident, + _ => unreachable!(), + }, + }) + .collect::>() + } + + let method = if sig.receiver().is_some() { + let idents = idents_with_skip(inputs.clone(), 1); + // First argument is the receiver, we skip it. + let inputs = inputs.skip(1); + + parse_quote! { + #[allow(non_snake_case)] + fn #mangled_name (self_: *mut #ty_str, #(#inputs),*) #out { + let self_ = unsafe { &mut *self_ }; + self_. #method_name (#(#idents),*) + } + } + } else if is_constructor { + let idents = idents_with_skip(inputs.clone(), 0); + parse_quote!( + #[allow(non_snake_case)] + fn #mangled_name (#(#inputs),*) #out { + #ty_str:: #method_name (#(#idents),*) + } + ) + } else { + return Err(util::Error::MissingReceiver); + }; + + let (generated, mut sym) = crate::fn_::handle_inner( + method, + crate::FnAttributes { + internal: true, + constructor: is_constructor, + ..Default::default() + }, + )?; + + // Set method name to the original name as the + // managed name is used for the internal symbol. + sym.set_name(method_name); + + methods.push(generated); + syms.push(quote::quote! { #sym }); + } + _ => {} + } + } + + // Generate a dealloc method. + { + let ident = format_ident!("__{}_dealloc", ty_str); + let dispose = parse_quote! { + #[allow(non_snake_case)] + fn #ident(self_: *mut #ty_str) { + if self_.is_null() { + return; + } + unsafe { drop(Box::from_raw(self_)) } + } + }; + let (generated, mut sym) = crate::fn_::handle_inner( + dispose, + crate::FnAttributes { + internal: true, + ..Default::default() + }, + )?; + + sym.set_name(format_ident!("dealloc")); + + methods.push(generated); + syms.push(quote::quote! { #sym }); + } + + Ok(quote::quote! { + #impl_ + #(#methods)* + const _: () = { + // Assert that the type implements `BindgenType`. + const fn _assert_impl() {} + _assert_impl::<#ty_str>(); + + #[deno_bindgen::linkme::distributed_slice(deno_bindgen::INVENTORY)] + pub static _B: deno_bindgen::Inventory = deno_bindgen::Inventory::Struct( + deno_bindgen::inventory::Struct { + name: stringify!(#ty_str), + methods: &[#syms], + } + ); + }; + }) +} diff --git a/deno_bindgen_macro/src/lib.rs b/deno_bindgen_macro/src/lib.rs index 39cbbac..ddb3709 100644 --- a/deno_bindgen_macro/src/lib.rs +++ b/deno_bindgen_macro/src/lib.rs @@ -1,233 +1,79 @@ // Copyright 2020-2021 the Deno authors. All rights reserved. MIT license. use proc_macro::TokenStream; -use quote::format_ident; -use quote::quote; -use std::env; -use std::fs::OpenOptions; -use std::io::Read; -use std::io::Write; -use std::path::Path; +use syn::meta::ParseNestedMeta; +use syn::parse2; use syn::parse_macro_input; -use syn::parse_quote; -use syn::ItemFn; +use syn::Item; -mod attrs; -mod derive_fn; -mod derive_struct; -mod docs; -mod meta; +mod fn_; +mod impl_; +mod struct_; +mod util; -use crate::derive_fn::process_function; -use crate::derive_struct::process_struct; -use crate::meta::Glue; -use crate::meta::Type; +#[derive(Default)] +pub(crate) struct FnAttributes { + pub(crate) non_blocking: bool, + pub(crate) constructor: bool, -#[cfg(target_endian = "little")] -const ENDIANNESS: bool = true; + pub(crate) internal: bool, +} -#[cfg(target_endian = "big")] -const ENDIANNESS: bool = false; +impl FnAttributes { + fn parse(&mut self, meta: ParseNestedMeta) -> syn::parse::Result<()> { + if meta.path.is_ident("non_blocking") { + self.non_blocking = true; + Ok(()) + } else { + Err(meta.error("unsupported attribute")) + } + } +} #[proc_macro_attribute] -pub fn deno_bindgen(attr: TokenStream, input: TokenStream) -> TokenStream { - let metafile_path: String = match env::var("OUT_DIR") { - Ok(out_dir) => Path::new(&out_dir) - .join("bindings.json") - .into_os_string() - .into_string() - .unwrap(), - Err(_e) => String::from("bindings.json"), - }; - - let mut metadata: Glue = - match OpenOptions::new().read(true).open(metafile_path.as_str()) { - Ok(mut fd) => { - let mut meta = String::new(); - fd.read_to_string(&mut meta) - .expect("Error reading meta file"); - - serde_json::from_str(&meta).unwrap_or_default() - } - Err(_) => Glue { - little_endian: ENDIANNESS, - name: env::var("CARGO_CRATE_NAME").unwrap_or_default(), - ..Default::default() - }, - }; - - let mut metafile = OpenOptions::new() - .write(true) - .create(true) - .open(metafile_path.as_str()) - .expect("Error opening meta file"); - - match syn::parse::(input.clone()) { - Ok(func) => { - let attr = parse_macro_input!(attr as syn::AttributeArgs); - let symbol = process_function(func.clone(), attr, &mut metadata).unwrap(); - - let mut params = vec![]; - let mut overrides = vec![]; - let mut input_idents = vec![]; - let mut c_index = 0; - - for parameter in symbol.parameters { - match parameter { - Type::StructEnum { .. } => { - let ident = format_ident!("arg{}", c_index.to_string()); - params.push(quote! { #ident: *const u8 }); - - c_index += 1; - let len_ident = format_ident!("arg{}", c_index.to_string()); - params.push(quote! { #len_ident: usize }); - - overrides.push(quote! { - let buf = unsafe { - ::std::slice::from_raw_parts(#ident, #len_ident) - }; - let #ident = deno_bindgen::serde_json::from_slice(buf).unwrap(); - }); - - input_idents.push(ident); - } - Type::Str | Type::Buffer | Type::BufferMut => { - let ident = format_ident!("arg{}", c_index.to_string()); - match parameter { - Type::Str | Type::Buffer => { - params.push(quote! { #ident: *const u8 }) - } - Type::BufferMut => params.push(quote! { #ident: *mut u8 }), - _ => unreachable!(), - }; - - c_index += 1; - let len_ident = format_ident!("arg{}", c_index.to_string()); - params.push(quote! { #len_ident: usize }); - - let return_type = match parameter { - Type::Str => quote! { ::std::str::from_utf8(buf).unwrap() }, - Type::Buffer | Type::BufferMut => quote! { buf }, - _ => unreachable!(), - }; - - let buf_expr = match parameter { - Type::Str | Type::Buffer => { - quote! { let buf = ::std::slice::from_raw_parts(#ident, #len_ident); } - } - Type::BufferMut => { - // https://github.com/littledivy/deno_bindgen/issues/26 - // *mut u8 should never outlive the symbol call. This can lead to UB. - quote! { let mut buf: &'sym mut [u8] = ::std::slice::from_raw_parts_mut(#ident, #len_ident); - } - } - _ => unreachable!(), - }; - - overrides.push(quote! { - let #ident = unsafe { - #buf_expr - #return_type - }; - }); - - input_idents.push(ident); - } - // TODO - _ => { - let ident = format_ident!("arg{}", c_index.to_string()); - let ty = syn::Type::from(parameter); - params.push(quote! { #ident: #ty }); - input_idents.push(ident); - } - }; - - c_index += 1; - } - - let (result, transformer) = match symbol.result { - Type::Buffer - // Note that this refers to an owned String - // and not a `&str` - | Type::Str => { - let ty = parse_quote! { *const u8 }; - let slice = match symbol.result { - Type::Str => quote! { - result.as_bytes() - }, - _ => quote! { result } - }; - let transformer = quote! { - let length = (result.len() as u32).to_be_bytes(); - let mut v = length.to_vec(); - v.extend_from_slice(&#slice); - - ::std::mem::forget(result); - let result = v.as_ptr(); - // Leak the result to JS land. - ::std::mem::forget(v); - result - }; - - (ty, transformer) - } - Type::StructEnum { .. } => { - let ty = parse_quote! { *const u8 }; - let transformer = quote! { - let json = deno_bindgen::serde_json::to_string(&result).expect("Failed to serialize as JSON"); - let encoded = json.into_bytes(); - let length = (encoded.len() as u32).to_be_bytes(); - let mut v = length.to_vec(); - v.extend(encoded.clone()); - - let ret = v.as_ptr(); - // Leak the result to JS land. - ::std::mem::forget(v); - ret - }; - - (ty, transformer) - } - Type::Ptr => (parse_quote! { *const u8 }, quote! { result }), - _ => (syn::Type::from(symbol.result), quote! { result }), - }; - - let name = &func.sig.ident; - let fn_inputs = &func.sig.inputs; - let fn_output = &func.sig.output; - let fn_generics = &func.sig.generics; - let fn_block = &func.block; - - let overrides = overrides - .iter() - .fold(quote! {}, |acc, new| quote! { #acc #new }); - - metafile - .write_all(&serde_json::to_vec(&metadata).unwrap()) - .unwrap(); - - TokenStream::from(quote! { - #[no_mangle] - pub extern "C" fn #name <'sym> (#(#params,) *) -> #result { - fn __inner_impl #fn_generics (#fn_inputs) #fn_output #fn_block - #overrides - let result = __inner_impl(#(#input_idents, ) *); - #transformer - } - }) +pub fn deno_bindgen(args: TokenStream, input: TokenStream) -> TokenStream { + match parse2::(input.into()).unwrap() { + Item::Fn(input) => { + let mut attrs = FnAttributes::default(); + let attrs_parser = syn::meta::parser(|meta| attrs.parse(meta)); + parse_macro_input!(args with attrs_parser); + + fn_::handle(input, attrs).unwrap().into() } - Err(_) => { - let input = syn::parse_macro_input!(input as syn::DeriveInput); - process_struct(&mut metadata, input.clone()).unwrap(); - - metafile - .write_all(&serde_json::to_vec(&metadata).unwrap()) - .unwrap(); + Item::Struct(input) => struct_::handle(input).unwrap().into(), + Item::Impl(input) => impl_::handle(input).unwrap().into(), + _ => panic!("only functions are supported"), + } +} - TokenStream::from(quote! { - #[derive(::serde::Deserialize,::serde::Serialize)] - #input - }) +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + #[testing_macros::fixture("tests/fn/*.test.rs")] + fn test_codegen_fn(input: PathBuf) { + let update_expected = std::env::var("UPDATE_EXPECTED").is_ok(); + + let source = + std::fs::read_to_string(&input).expect("failed to read test case"); + let item_fn = syn::parse_str::(&source) + .expect("failed to parse test case"); + + let tokens = crate::fn_::handle(item_fn, Default::default()).unwrap(); + let tree = syn::parse2(tokens).unwrap(); + let actual = prettyplease::unparse(&tree); + + let expected_out = input.with_extension("out.rs"); + if update_expected { + std::fs::write(expected_out, actual) + .expect("Failed to write expectation file"); + } else { + let expected = std::fs::read_to_string(expected_out) + .expect("Failed to read expectation file"); + assert_eq!( + expected, actual, + "Failed to match expectation. Use UPDATE_EXPECTED=1." + ); } } } diff --git a/deno_bindgen_macro/src/meta.rs b/deno_bindgen_macro/src/meta.rs deleted file mode 100644 index 247cb66..0000000 --- a/deno_bindgen_macro/src/meta.rs +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2020-2021 the Deno authors. All rights reserved. MIT license. - -use serde::Deserialize; -use serde::Serialize; -use std::collections::HashMap; -use syn::parse_quote; - -#[derive(Serialize, Deserialize, Clone)] -#[serde(rename_all = "lowercase")] -pub enum Type { - /// Straight forward types supported - /// by Deno's FFI - I8, - U8, - I16, - U16, - I32, - U32, - I64, - U64, - F32, - F64, - Usize, - Isize, - Void, - - /// Types that pave way for - /// serializers. buffers <3 - Buffer, - BufferMut, - Str, - Ptr, - - /// Not-so straightforward types that - /// `deno_bingen` maps to. - StructEnum { - ident: String, - }, -} - -impl From for syn::Type { - fn from(ty: Type) -> Self { - match ty { - Type::I8 => parse_quote! { i8 }, - Type::U8 => parse_quote! { u8 }, - Type::I16 => parse_quote! { i16 }, - Type::U16 => parse_quote! { u16 }, - Type::I32 => parse_quote! { i32 }, - Type::U32 => parse_quote! { u32 }, - Type::I64 => parse_quote! { i64 }, - Type::U64 => parse_quote! { u64 }, - Type::F32 => parse_quote! { f32 }, - Type::F64 => parse_quote! { f64 }, - Type::Usize => parse_quote! { usize }, - Type::Isize => parse_quote! { isize }, - Type::Void => parse_quote! { () }, - _ => unreachable!(), - } - } -} - -#[derive(Serialize, Deserialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Symbol { - pub parameters: Vec, - pub result: Type, - pub non_blocking: bool, -} - -#[derive(Serialize, Deserialize, Default)] -#[serde(rename_all = "camelCase")] -pub struct Glue { - pub name: String, - pub little_endian: bool, - pub symbols: HashMap, - pub type_defs: HashMap>, - pub ts_types: HashMap, -} diff --git a/deno_bindgen_macro/src/struct_.rs b/deno_bindgen_macro/src/struct_.rs new file mode 100644 index 0000000..0891261 --- /dev/null +++ b/deno_bindgen_macro/src/struct_.rs @@ -0,0 +1,26 @@ +use proc_macro2::TokenStream as TokenStream2; +use syn::ItemStruct; + +use crate::util::Result; +use crate::util::{self}; + +pub fn handle(struct_: ItemStruct) -> Result { + if struct_.generics.params.first().is_some() { + return Err(util::Error::Generics); + } + + if struct_.generics.where_clause.is_some() { + return Err(util::Error::WhereClause); + } + + let ref ty_str @ _ = struct_.ident; + Ok(quote::quote! { + #struct_ + + impl ::deno_bindgen::BindgenType for #ty_str { + fn type_name() -> &'static str { + stringify!(#ty_str) + } + } + }) +} diff --git a/deno_bindgen_macro/src/util.rs b/deno_bindgen_macro/src/util.rs new file mode 100644 index 0000000..811047f --- /dev/null +++ b/deno_bindgen_macro/src/util.rs @@ -0,0 +1,26 @@ +#[derive(Debug)] +pub enum Error { + Asyncness, + Reciever, + UnsupportedType, + Generics, + WhereClause, + MissingReceiver, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Error::Asyncness => write!(f, "async functions are not supported"), + Error::Reciever => write!(f, "methods are not supported"), + Error::UnsupportedType => write!(f, "unsupported type"), + Error::Generics => write!(f, "generics are not supported"), + Error::WhereClause => write!(f, "where clauses are not supported"), + Error::MissingReceiver => write!(f, "missing receiver"), + } + } +} + +impl std::error::Error for Error {} + +pub type Result = std::result::Result; diff --git a/deno_bindgen_macro/tests/fn/add.test.out.rs b/deno_bindgen_macro/tests/fn/add.test.out.rs new file mode 100644 index 0000000..7a70eb1 --- /dev/null +++ b/deno_bindgen_macro/tests/fn/add.test.out.rs @@ -0,0 +1,19 @@ +const _: () = { + #[deno_bindgen::linkme::distributed_slice(deno_bindgen::INVENTORY)] + pub static _A: deno_bindgen::Inventory = deno_bindgen::Inventory::Symbol(deno_bindgen::Symbol { + name: stringify!(add), + parameters: &[deno_bindgen::Type::Int32, deno_bindgen::Type::Int32], + return_type: deno_bindgen::Type::Int32, + non_blocking: false, + internal: false, + is_constructor: false, + }); +}; +#[no_mangle] +extern "C" fn add(a: i32, b: i32) -> i32 { + fn add(a: i32, b: i32) -> i32 { + a + b + } + let ret = add(a, b); + ret +} diff --git a/deno_bindgen_macro/tests/fn/add.test.rs b/deno_bindgen_macro/tests/fn/add.test.rs new file mode 100644 index 0000000..52d1530 --- /dev/null +++ b/deno_bindgen_macro/tests/fn/add.test.rs @@ -0,0 +1 @@ +fn add(a: i32, b: i32) -> i32 { a + b } \ No newline at end of file diff --git a/deno_bindgen_macro/tests/fn/buffer.test.out.rs b/deno_bindgen_macro/tests/fn/buffer.test.out.rs new file mode 100644 index 0000000..8916651 --- /dev/null +++ b/deno_bindgen_macro/tests/fn/buffer.test.out.rs @@ -0,0 +1,24 @@ +const _: () = { + #[deno_bindgen::linkme::distributed_slice(deno_bindgen::INVENTORY)] + pub static _A: deno_bindgen::Inventory = deno_bindgen::Inventory::Symbol(deno_bindgen::Symbol { + name: stringify!(write_hello), + parameters: &[deno_bindgen::Type::Buffer], + return_type: deno_bindgen::Type::Void, + non_blocking: false, + internal: false, + is_constructor: false, + }); +}; +#[no_mangle] +extern "C" fn write_hello(__arg_0: *const (), __arg_1: u32) { + fn write_hello(buf: &mut [u8]) { + buf[0] = b'H'; + buf[1] = b'e'; + buf[2] = b'l'; + buf[3] = b'l'; + buf[4] = b'o'; + } + let buf = unsafe { std::slice::from_raw_parts_mut(__arg_0 as _, __arg_1 as usize) }; + let ret = write_hello(buf); + ret +} diff --git a/deno_bindgen_macro/tests/fn/buffer.test.rs b/deno_bindgen_macro/tests/fn/buffer.test.rs new file mode 100644 index 0000000..5ef14e2 --- /dev/null +++ b/deno_bindgen_macro/tests/fn/buffer.test.rs @@ -0,0 +1,7 @@ +fn write_hello(buf: &mut [u8]) { + buf[0] = b'H'; + buf[1] = b'e'; + buf[2] = b'l'; + buf[3] = b'l'; + buf[4] = b'o'; +} \ No newline at end of file diff --git a/deno_bindgen_macro/tests/fn/pointer.test.out.rs b/deno_bindgen_macro/tests/fn/pointer.test.out.rs new file mode 100644 index 0000000..fb7f12c --- /dev/null +++ b/deno_bindgen_macro/tests/fn/pointer.test.out.rs @@ -0,0 +1,21 @@ +const _: () = { + #[deno_bindgen::linkme::distributed_slice(deno_bindgen::INVENTORY)] + pub static _A: deno_bindgen::Inventory = deno_bindgen::Inventory::Symbol(deno_bindgen::Symbol { + name: stringify!(is_utf8), + parameters: &[deno_bindgen::Type::Pointer, deno_bindgen::Type::Uint64], + return_type: deno_bindgen::Type::Int32, + non_blocking: false, + internal: false, + is_constructor: false, + }); +}; +#[no_mangle] +extern "C" fn is_utf8(__arg_0: *const (), len: usize) -> i32 { + fn is_utf8(ptr: *const u8, len: usize) -> i32 { + std::str::from_utf8(unsafe { std::slice::from_raw_parts(ptr, len) }).is_ok() + as i32 + } + let ptr = __arg_0 as _; + let ret = is_utf8(ptr, len); + ret +} diff --git a/deno_bindgen_macro/tests/fn/pointer.test.rs b/deno_bindgen_macro/tests/fn/pointer.test.rs new file mode 100644 index 0000000..bc9ffab --- /dev/null +++ b/deno_bindgen_macro/tests/fn/pointer.test.rs @@ -0,0 +1,3 @@ +fn is_utf8(ptr: *const u8, len: usize) -> i32 { + std::str::from_utf8(unsafe { std::slice::from_raw_parts(ptr, len) }).is_ok() as i32 +} \ No newline at end of file diff --git a/e2e_test/Cargo.lock b/e2e_test/Cargo.lock new file mode 100644 index 0000000..de1faa5 --- /dev/null +++ b/e2e_test/Cargo.lock @@ -0,0 +1,203 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "deno_bindgen" +version = "0.8.1" +dependencies = [ + "deno_bindgen_ir", + "deno_bindgen_macro", + "linkme", + "serde", + "serde_json", +] + +[[package]] +name = "deno_bindgen_e2e" +version = "0.1.0" +dependencies = [ + "deno_bindgen", + "linkme", + "serde", +] + +[[package]] +name = "deno_bindgen_ir" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "deno_bindgen_macro" +version = "0.8.1" +dependencies = [ + "Inflector", + "deno_bindgen_ir", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "linkme" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ed2ee9464ff9707af8e9ad834cffa4802f072caad90639c583dd3c62e6e608" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba125974b109d512fccbc6c0244e7580143e460895dfd6ea7f8bbb692fd94396" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "serde" +version = "1.0.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "2.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/e2e_test/Cargo.toml b/e2e_test/Cargo.toml new file mode 100644 index 0000000..fb8f652 --- /dev/null +++ b/e2e_test/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "deno_bindgen_e2e" +version = "0.1.0" +edition = "2018" + +[dependencies] +deno_bindgen = { path = "../deno_bindgen/" } +serde = { version = "1", features = ["derive"] } +linkme = "0.3" + +[lib] +name = "deno_bindgen_e2e" +crate-type = ["cdylib"] diff --git a/e2e_test/bench.js b/e2e_test/bench.js new file mode 100644 index 0000000..10d472c --- /dev/null +++ b/e2e_test/bench.js @@ -0,0 +1,12 @@ +import { add, bytelen, Foo, make_foo } from "./bindings/mod.ts"; + +Deno.bench("add", () => add(1, 2)); + +const b = new Uint8Array([1, 2, 3, 4]); +Deno.bench("bytelen", () => bytelen(b)); + +Deno.bench("make_foo", () => make_foo(21)); +Deno.bench("new Foo", () => new Foo(21)); + +const foo = new Foo(21); +Deno.bench("Foo#bar", () => foo.bar(1)); diff --git a/e2e_test/bindings/mod.ts b/e2e_test/bindings/mod.ts new file mode 100644 index 0000000..4071862 --- /dev/null +++ b/e2e_test/bindings/mod.ts @@ -0,0 +1,301 @@ +// deno-lint-ignore-file + +// This file is automatically generated by deno_bindgen. +// Do not edit this file directly. + +const { dlopen } = Deno; + +const { symbols } = dlopen('/Users/divy/gh/deno_bindgen/e2e_test/target/debug/libdeno_bindgen_e2e.dylib', { + add: { + parameters: [ + 'i32', + 'i32', + ], + result: 'i32', + nonblocking: false + }, + __Input_new: { + parameters: [ + 'i32', + 'i32', + ], + result: 'pointer', + nonblocking: false + }, + __Input_dealloc: { + parameters: [ + 'pointer', + ], + result: 'void', + nonblocking: false + }, + add2: { + parameters: [ + 'pointer', + ], + result: 'i32', + nonblocking: false + }, + bytelen: { + parameters: [ + 'buffer', + 'usize', + ], + result: 'u32', + nonblocking: false + }, + buf_mut: { + parameters: [ + 'buffer', + 'usize', + ], + result: 'void', + nonblocking: false + }, + cstr: { + parameters: [], + result: 'pointer', + nonblocking: false + }, + strlen: { + parameters: [ + 'pointer', + ], + result: 'u32', + nonblocking: false + }, + non_blocking: { + parameters: [], + result: 'i32', + nonblocking: true + }, + make_foo: { + parameters: [], + result: 'pointer', + nonblocking: false + }, + inc_foo: { + parameters: [ + 'pointer', + ], + result: 'void', + nonblocking: false + }, + __Foo_new: { + parameters: [ + 'u32', + ], + result: 'pointer', + nonblocking: false + }, + __Foo_inc: { + parameters: [ + 'pointer', + ], + result: 'void', + nonblocking: false + }, + __Foo_bar: { + parameters: [ + 'pointer', + 'u32', + ], + result: 'u32', + nonblocking: false + }, + __Foo_dealloc: { + parameters: [ + 'pointer', + ], + result: 'void', + nonblocking: false + }, +}); + +export function add( + arg0: number, + arg1: number, +): number { + return symbols.add( + arg0, + arg1, + ) +} + +function __Input_new( + arg0: number, + arg1: number, +): Input { + const ret = symbols.__Input_new( + arg0, + arg1, + ) + return Input.__constructor(ret); +} + +function __Input_dealloc( + arg0: Deno.PointerObject | null, +): void { + return symbols.__Input_dealloc( + arg0, + ) +} + +export class Input { + ptr: Deno.PointerObject | null = null; + + static __constructor(ptr: Deno.PointerObject | null) { + const self = Object.create(Input.prototype); + self.ptr = ptr; + return self; + } + + [Symbol.dispose]() { + this.dealloc(); + this.ptr = null; + } + + constructor(arg0: number, arg1: number) { + return __Input_new( + arg0, + arg1, + ) + } + + dealloc(): void { + return __Input_dealloc( + this.ptr, + ) + } +} + +export function add2( + arg0: Input, +): number { + return symbols.add2( + arg0.ptr, + ) +} + +export function bytelen( + arg0: Uint8Array, +): number { + return symbols.bytelen( + arg0, + arg0.byteLength, + ) +} + +export function buf_mut( + arg0: Uint8Array, +): void { + return symbols.buf_mut( + arg0, + arg0.byteLength, + ) +} + +export function cstr(): Deno.PointerObject | null { + return symbols.cstr() +} + +export function strlen( + arg0: Deno.PointerObject | null, +): number { + return symbols.strlen( + arg0, + ) +} + +export function non_blocking(): Promise { + return symbols.non_blocking() +} + +export function make_foo(): Foo { + const ret = symbols.make_foo() + return Foo.__constructor(ret); +} + +export function inc_foo( + arg0: Foo, +): void { + return symbols.inc_foo( + arg0.ptr, + ) +} + +function __Foo_new( + arg0: number, +): Foo { + const ret = symbols.__Foo_new( + arg0, + ) + return Foo.__constructor(ret); +} + +function __Foo_inc( + arg0: Deno.PointerObject | null, +): void { + return symbols.__Foo_inc( + arg0, + ) +} + +function __Foo_bar( + arg0: Deno.PointerObject | null, + arg1: number, +): number { + return symbols.__Foo_bar( + arg0, + arg1, + ) +} + +function __Foo_dealloc( + arg0: Deno.PointerObject | null, +): void { + return symbols.__Foo_dealloc( + arg0, + ) +} + +export class Foo { + ptr: Deno.PointerObject | null = null; + + static __constructor(ptr: Deno.PointerObject | null) { + const self = Object.create(Foo.prototype); + self.ptr = ptr; + return self; + } + + [Symbol.dispose]() { + this.dealloc(); + this.ptr = null; + } + + constructor(arg0: number) { + return __Foo_new( + arg0, + ) + } + + inc(): void { + return __Foo_inc( + this.ptr, + ) + } + + bar(arg0: number): number { + return __Foo_bar( + this.ptr, + arg0, + ) + } + + dealloc(): void { + return __Foo_dealloc( + this.ptr, + ) + } +} + diff --git a/e2e_test/bindings_test.ts b/e2e_test/bindings_test.ts new file mode 100644 index 0000000..dc46723 --- /dev/null +++ b/e2e_test/bindings_test.ts @@ -0,0 +1,114 @@ +import { + add, + add2, + buf_mut, + bytelen, + cstr, + Foo, + inc_foo, + Input, + make_foo, + non_blocking, + strlen, +} from "./bindings/mod.ts"; +import { + assert, + assertEquals, +} from "https://deno.land/std@0.178.0/testing/asserts.ts"; + +Deno.test({ + name: "add#test", + fn: () => { + assertEquals(add(1, 2), 3); + + using input = new Input(-1, 1); + assertEquals(add2(input), 0); + }, +}); + +Deno.test({ + name: "bytelen#test", + fn: () => { + assertEquals(bytelen(new TextEncoder().encode("hello")), 5); + }, +}); + +Deno.test({ + name: "buf_mut#test", + fn: () => { + const buf = new Uint8Array(1); + buf_mut(buf); + assertEquals(buf[0], 99); + }, +}); + +Deno.test({ + name: "cstr#test", + fn: () => { + const ptr = cstr(); + const str = Deno.UnsafePointerView.getCString(ptr!); + assertEquals(str, "Hello, World!"); + }, +}); + +Deno.test({ + name: "strlen#test", + fn: () => { + const ptr = strlen(cstr()); + assertEquals(ptr, 13); + }, +}); + +Deno.test({ + name: "non_blocking#test", + fn: async () => { + const result = await non_blocking(); + assertEquals(result, 42); + }, +}); + +Deno.test({ + name: "make_foo#test", + fn: () => { + const foo = make_foo(); + assert(foo instanceof Foo); + assertEquals(foo.bar(1), 43); + }, +}); + +Deno.test({ + name: "Foo#constructor", + fn() { + const foo = new Foo(42); + assertEquals(foo.bar(1), 43); + }, +}); + +Deno.test({ + name: "Foo#using", + fn() { + using foo = new Foo(1); + foo.inc(); + assertEquals(foo.bar(1), 3); + }, +}); + +Deno.test({ + name: "Foo#using explicit", + fn() { + using foo = make_foo(); + + // Multiple dipose calls are nop. + foo[Symbol.dispose](); + foo[Symbol.dispose](); + }, +}); + +Deno.test({ + name: "inc_foo#test", + fn: () => { + using foo = new Foo(22); + inc_foo(foo); + assertEquals(foo.bar(0), 23); + }, +}); diff --git a/e2e_test/src/lib.rs b/e2e_test/src/lib.rs new file mode 100644 index 0000000..6fbc208 --- /dev/null +++ b/e2e_test/src/lib.rs @@ -0,0 +1,87 @@ +use deno_bindgen::deno_bindgen; + +#[deno_bindgen] +fn add(a: i32, b: i32) -> i32 { + a + b +} + +#[deno_bindgen] +struct Input { + a: i32, + b: i32, +} + +#[deno_bindgen] +impl Input { + #[constructor] + fn new(a: i32, b: i32) -> Input { + Input { a, b } + } +} + +#[deno_bindgen] +fn add2(input: &Input) -> i32 { + input.a + input.b +} + +#[deno_bindgen] +fn bytelen(b: &[u8]) -> u32 { + b.len() as u32 +} + +#[deno_bindgen] +fn buf_mut(b: &mut [u8]) { + b[0] = 99; +} + +#[deno_bindgen] +fn cstr() -> *const u8 { + b"Hello, World!\0".as_ptr() +} + +#[deno_bindgen] +fn strlen(s: *const u8) -> u32 { + let mut len = 0; + unsafe { + while *s.add(len as usize) != 0 { + len += 1; + } + } + len +} + +#[deno_bindgen(non_blocking)] +fn non_blocking() -> i32 { + 42 +} + +#[deno_bindgen] +fn make_foo() -> Foo { + Foo { internal: 42 } +} + +#[deno_bindgen] +fn inc_foo(foo: &mut Foo) { + foo.internal += 1; +} + +#[deno_bindgen] +pub struct Foo { + internal: u32, +} + +#[deno_bindgen] +impl Foo { + #[constructor] + fn new(internal: u32) -> Foo { + Foo { internal } + } + + fn inc(&mut self) { + self.internal += 1; + } + + fn bar(&self, a: u32) -> u32 { + self.internal + a + } +} \ No newline at end of file diff --git a/example/Cargo.toml b/example/Cargo.toml index 8bc71e7..9b7aada 100644 --- a/example/Cargo.toml +++ b/example/Cargo.toml @@ -1,12 +1,15 @@ [package] -name = "deno_bindgen_test" +name = "usb_example" version = "0.1.0" edition = "2018" [dependencies] deno_bindgen = { path = "../deno_bindgen/" } +webusb = "0.5.0" serde = { version = "1", features = ["derive"] } +linkme = "0.3" [lib] -name = "deno_bindgen_test" +name = "deno_usb" +path = "./lib.rs" crate-type = ["cdylib"] diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..4435c77 --- /dev/null +++ b/example/README.md @@ -0,0 +1,13 @@ +## `deno_usb_example` + +```bash +$> deno_bindgen -o mod.ts + Finished dev [unoptimized + debuginfo] target(s) in 0.02s +Initializing usb_example +Ready usb_example + +$> deno run --allow-ffi --unstable lsusb.ts +Product Name: G102 LIGHTSYNC Gaming Mouse +Vendor ID: 1133 +Product ID: 49298 +``` diff --git a/example/bench.js b/example/bench.js deleted file mode 100644 index 9250eaf..0000000 --- a/example/bench.js +++ /dev/null @@ -1,12 +0,0 @@ -import { add, test_buf, test_serde, test_str } from "./bindings/bindings.ts"; - -// Optimized fast paths: -Deno.bench("add", () => add(1, 2)); -const b = new Uint8Array([1, 2, 3, 4]); -Deno.bench("test_buf", () => test_buf(b)); - -// Unoptimized paths: -Deno.bench("test_str", () => test_str("hello")); -Deno.bench("test_serde", () => { - test_serde({ arr: ["IT", "WORKS"] }); -}); diff --git a/example/bindings/bindings.ts b/example/bindings/bindings.ts deleted file mode 100644 index e33007b..0000000 --- a/example/bindings/bindings.ts +++ /dev/null @@ -1,327 +0,0 @@ -// Auto-generated with deno_bindgen -function encode(v: string | Uint8Array): Uint8Array { - if (typeof v !== "string") return v - return new TextEncoder().encode(v) -} - -function decode(v: Uint8Array): string { - return new TextDecoder().decode(v) -} - -// deno-lint-ignore no-explicit-any -function readPointer(v: any): Uint8Array { - const ptr = new Deno.UnsafePointerView(v) - const lengthBe = new Uint8Array(4) - const view = new DataView(lengthBe.buffer) - ptr.copyInto(lengthBe, 0) - const buf = new Uint8Array(view.getUint32(0)) - ptr.copyInto(buf, 4) - return buf -} - -const url = new URL("../target/release", import.meta.url) - -import { dlopen, FetchOptions } from "https://deno.land/x/plug@1.0.1/mod.ts" -let uri = url.toString() -if (!uri.endsWith("/")) uri += "/" - -let darwin: string | { aarch64: string; x86_64: string } = uri - -const opts: FetchOptions = { - name: "deno_bindgen_test", - url: { - darwin, - windows: uri, - linux: uri, - }, - suffixes: { - darwin: { - aarch64: "_arm64", - }, - }, - cache: "use", -} -const { symbols } = await dlopen(opts, { - add: { parameters: ["i32", "i32"], result: "i32", nonblocking: false }, - add2: { parameters: ["buffer", "usize"], result: "i32", nonblocking: false }, - add3: { parameters: ["f32", "f32"], result: "f32", nonblocking: false }, - add4: { parameters: ["f64", "f64"], result: "f64", nonblocking: false }, - add5: { - parameters: ["buffer", "usize", "buffer", "usize"], - result: "buffer", - nonblocking: false, - }, - add6: { - parameters: ["buffer", "usize", "buffer", "usize"], - result: "buffer", - nonblocking: false, - }, - sleep: { parameters: ["u64"], result: "void", nonblocking: true }, - test_buf: { - parameters: ["buffer", "usize"], - result: "u8", - nonblocking: false, - }, - test_buffer_return: { - parameters: ["buffer", "usize"], - result: "buffer", - nonblocking: false, - }, - test_buffer_return_async: { - parameters: ["buffer", "usize"], - result: "buffer", - nonblocking: true, - }, - test_hashmap: { parameters: [], result: "buffer", nonblocking: false }, - test_lifetime: { - parameters: ["buffer", "usize"], - result: "usize", - nonblocking: false, - }, - test_manual_ptr: { parameters: [], result: "buffer", nonblocking: false }, - test_manual_ptr_async: { - parameters: [], - result: "buffer", - nonblocking: true, - }, - test_mixed: { - parameters: ["isize", "buffer", "usize"], - result: "i32", - nonblocking: false, - }, - test_mixed_order: { - parameters: ["i32", "buffer", "usize", "i32"], - result: "i32", - nonblocking: false, - }, - test_mut_buf: { - parameters: ["buffer", "usize"], - result: "void", - nonblocking: false, - }, - test_output: { parameters: [], result: "buffer", nonblocking: false }, - test_output_async: { parameters: [], result: "buffer", nonblocking: true }, - test_reserved_field: { parameters: [], result: "buffer", nonblocking: false }, - test_serde: { - parameters: ["buffer", "usize"], - result: "u8", - nonblocking: false, - }, - test_str: { - parameters: ["buffer", "usize"], - result: "void", - nonblocking: false, - }, - test_str_ret: { parameters: [], result: "buffer", nonblocking: false }, - test_tag_and_content: { - parameters: ["buffer", "usize"], - result: "i32", - nonblocking: false, - }, -}) -/** - * Doc comment for `Input` struct. - * ...testing multiline - */ -export type Input = { - /** - * Doc comments get - * transformed to JS doc - * comments. - */ - a: number - b: number -} -export type MyStruct = { - arr: Array -} -export type OptionStruct = { - maybe: string | undefined | null -} -export type PlainEnum = - | { - a: { - _a: string - } - } - | "b" - | "c" -export type TagAndContent = - | { key: "A"; value: { b: number } } - | { key: "C"; value: { d: number } } -export type TestLifetimeEnums = { - Text: { - _text: string - } -} -export type TestLifetimeWrap = { - _a: TestLifetimeEnums -} -export type TestLifetimes = { - text: string -} -export type TestReservedField = { - type: number - ref: number -} -export type WithRecord = { - my_map: Record -} -export function add(a0: number, a1: number) { - const rawResult = symbols.add(a0, a1) - const result = rawResult - return result -} -export function add2(a0: Input) { - const a0_buf = encode(JSON.stringify(a0)) - - const rawResult = symbols.add2(a0_buf, a0_buf.byteLength) - const result = rawResult - return result -} -export function add3(a0: number, a1: number) { - const rawResult = symbols.add3(a0, a1) - const result = rawResult - return result -} -export function add4(a0: number, a1: number) { - const rawResult = symbols.add4(a0, a1) - const result = rawResult - return result -} -export function add5(a0: Uint8Array, a1: Uint8Array) { - const a0_buf = encode(a0) - const a1_buf = encode(a1) - - const rawResult = symbols.add5( - a0_buf, - a0_buf.byteLength, - a1_buf, - a1_buf.byteLength, - ) - const result = readPointer(rawResult) - return result -} -export function add6(a0: Uint8Array, a1: Uint8Array) { - const a0_buf = encode(a0) - const a1_buf = encode(a1) - - const rawResult = symbols.add6( - a0_buf, - a0_buf.byteLength, - a1_buf, - a1_buf.byteLength, - ) - const result = readPointer(rawResult) - return result -} -export function sleep(a0: bigint) { - const rawResult = symbols.sleep(a0) - const result = rawResult - return result -} -export function test_buf(a0: Uint8Array) { - const a0_buf = encode(a0) - - const rawResult = symbols.test_buf(a0_buf, a0_buf.byteLength) - const result = rawResult - return result -} -export function test_buffer_return(a0: Uint8Array) { - const a0_buf = encode(a0) - - const rawResult = symbols.test_buffer_return(a0_buf, a0_buf.byteLength) - const result = readPointer(rawResult) - return result -} -export function test_buffer_return_async(a0: Uint8Array) { - const a0_buf = encode(a0) - - const rawResult = symbols.test_buffer_return_async(a0_buf, a0_buf.byteLength) - const result = rawResult.then(readPointer) - return result -} -export function test_hashmap() { - const rawResult = symbols.test_hashmap() - const result = readPointer(rawResult) - return JSON.parse(decode(result)) as WithRecord -} -export function test_lifetime(a0: TestLifetimes) { - const a0_buf = encode(JSON.stringify(a0)) - - const rawResult = symbols.test_lifetime(a0_buf, a0_buf.byteLength) - const result = rawResult - return result -} -export function test_manual_ptr() { - const rawResult = symbols.test_manual_ptr() - const result = readPointer(rawResult) - return result -} -export function test_manual_ptr_async() { - const rawResult = symbols.test_manual_ptr_async() - const result = rawResult.then(readPointer) - return result -} -export function test_mixed(a0: bigint, a1: Input) { - const a1_buf = encode(JSON.stringify(a1)) - - const rawResult = symbols.test_mixed(a0, a1_buf, a1_buf.byteLength) - const result = rawResult - return result -} -export function test_mixed_order(a0: number, a1: Input, a2: number) { - const a1_buf = encode(JSON.stringify(a1)) - - const rawResult = symbols.test_mixed_order(a0, a1_buf, a1_buf.byteLength, a2) - const result = rawResult - return result -} -export function test_mut_buf(a0: Uint8Array) { - const a0_buf = encode(a0) - - const rawResult = symbols.test_mut_buf(a0_buf, a0_buf.byteLength) - const result = rawResult - return result -} -export function test_output() { - const rawResult = symbols.test_output() - const result = readPointer(rawResult) - return JSON.parse(decode(result)) as Input -} -export function test_output_async() { - const rawResult = symbols.test_output_async() - const result = rawResult.then(readPointer) - return result.then(r => JSON.parse(decode(r))) as Promise -} -export function test_reserved_field() { - const rawResult = symbols.test_reserved_field() - const result = readPointer(rawResult) - return JSON.parse(decode(result)) as TestReservedField -} -export function test_serde(a0: MyStruct) { - const a0_buf = encode(JSON.stringify(a0)) - - const rawResult = symbols.test_serde(a0_buf, a0_buf.byteLength) - const result = rawResult - return result -} -export function test_str(a0: string) { - const a0_buf = encode(a0) - - const rawResult = symbols.test_str(a0_buf, a0_buf.byteLength) - const result = rawResult - return result -} -export function test_str_ret() { - const rawResult = symbols.test_str_ret() - const result = readPointer(rawResult) - return decode(result) -} -export function test_tag_and_content(a0: TagAndContent) { - const a0_buf = encode(JSON.stringify(a0)) - - const rawResult = symbols.test_tag_and_content(a0_buf, a0_buf.byteLength) - const result = rawResult - return result -} diff --git a/example/bindings_test.ts b/example/bindings_test.ts deleted file mode 100644 index 343baeb..0000000 --- a/example/bindings_test.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { - add, - add2, - add3, - add4, - add5, - add6, - OptionStruct, - sleep, - test_buf, - test_buffer_return, - test_buffer_return_async, - test_hashmap, - test_lifetime, - test_manual_ptr, - test_manual_ptr_async, - test_mixed, - test_mixed_order, - test_mut_buf, - test_output, - test_output_async, - test_reserved_field, - test_serde, - test_str, - test_str_ret, - test_tag_and_content, - TestReservedField, - WithRecord, -} from "./bindings/bindings.ts"; -import { assert, assertEquals } from "https://deno.land/std/testing/asserts.ts"; - -Deno.test({ - name: "add#test", - fn: () => { - assertEquals(add(1, 2), 3); - assertEquals(add(-1, 1), 0); - }, -}); - -Deno.test({ - name: "add2#test", - fn: () => { - assertEquals(add2({ a: 1, b: 2 }), 3); - }, -}); - -Deno.test({ - name: "add#test3", - fn: () => { - // If the argument type of Rust is f32, the calculation result may be different. - // Number in Java Script is float64, when data is passed to Rust, it becomes float32, so the number may change. - // e.g: `1.3 + 1.5` will be `2.799999952316284` - assertEquals(add3(1.5, 1.5), 3.0); - }, -}); - -Deno.test({ - name: "add#test4", - fn: () => { - assertEquals(add4(1.5, 1.3), 2.8); - }, -}); - -Deno.test({ - name: "add#test5", - fn: () => { - assertEquals( - add5(new Uint8Array([1, 2]), new Uint8Array([3, 4])), - new Uint8Array([1, 2, 3, 4]), - ); - }, -}); - -Deno.test({ - name: "add#test6", - fn: () => { - assertEquals( - add6(new Uint8Array([1, 2]), new Uint8Array([3, 4])), - new Uint8Array([1, 2, 3, 4]), - ); - }, -}); - -Deno.test({ - name: "sleep#test", - fn: async () => { - const now = performance.now(); - await sleep(50n); - assert(performance.now() - now > 50); - }, -}); - -Deno.test({ - name: "test_mixed#test", - fn: () => { - assertEquals(test_mixed(10n, { a: 10, b: 20 }), 20); - }, -}); - -Deno.test({ - name: "test_serde#test", - fn: () => { - assertEquals(test_serde({ arr: ["IT", "WORKS"] }), 1); - }, -}); - -Deno.test({ - name: "test_mixed_order#test", - fn: () => { - assertEquals(test_mixed_order(10, { a: 10, b: 0 }, 10), 30); - }, -}); - -Deno.test({ - name: "test_options#test", - fn: () => { - let opts: OptionStruct = { maybe: " " }; - opts.maybe = null; - opts.maybe = undefined; - }, -}); - -Deno.test({ - name: "test_str#test", - fn: () => { - let str = "Hello, World!"; - test_str(str); - }, -}); - -Deno.test({ - name: "test_buf#test", - fn: () => { - let buf = new Uint8Array([1, 0, 1]); - assertEquals(test_buf(buf), 1); - }, -}); - -Deno.test({ - name: "test_mut_buf#test", - fn: () => { - let u8 = new Uint8Array([0, 1, 2]); - assertEquals(u8[0], 0); - - test_mut_buf(u8); - assertEquals(u8[0], 69); - }, -}); - -Deno.test({ - name: "test_lifetime_struct#test", - fn: () => { - const TEXT = "Hello, World!"; - assertEquals(test_lifetime({ text: TEXT }), TEXT.length); - }, -}); - -Deno.test({ - name: "test_tag_and_content#test", - fn: () => { - assertEquals(test_tag_and_content({ key: "A", value: { b: 10 } }), 10); - - // test_tag_and_content returns -1 when enum variant isn't TagAndContent::A - assertEquals(test_tag_and_content({ key: "C", value: { d: 10 } }), -1); - }, -}); - -Deno.test({ - name: "test_buffer_return#test", - fn: () => { - const buf = test_buffer_return(new Uint8Array([1, 2, 3])); - - assertEquals(buf.byteLength, 3); - assertEquals(buf[0], 1); - assertEquals(buf[1], 2); - assertEquals(buf[2], 3); - }, -}); - -Deno.test({ - name: "test_buffer_return_async#test", - fn: async () => { - const buf = await test_buffer_return_async(new Uint8Array([1, 2, 3])); - - assertEquals(buf.byteLength, 3); - assertEquals(buf[0], 1); - assertEquals(buf[1], 2); - assertEquals(buf[2], 3); - }, -}); - -Deno.test({ - name: "test_manual_ptr#test", - fn: () => { - const buf = test_manual_ptr(); - const val = new TextDecoder().decode(buf); - - assertEquals(val, "test"); - }, -}); - -Deno.test({ - name: "test_manual_ptr_async#test", - fn: async () => { - const buf = await test_manual_ptr_async(); - const val = new TextDecoder().decode(buf); - - assertEquals(val, "test"); - }, -}); - -Deno.test({ - name: "test_output#test", - fn: () => { - const obj = test_output(); - - assertEquals(obj.a, 1); - assertEquals(obj.b, 2); - }, -}); - -Deno.test({ - name: "test_output_async#test", - fn: async () => { - const obj = await test_output_async(); - - assertEquals(obj.a, 3); - assertEquals(obj.b, 4); - }, -}); - -Deno.test({ - name: "test_reserved_field#test", - fn: () => { - const obj = test_reserved_field(); - - assertEquals(obj.type, 1); - assertEquals(obj.ref, 2); - }, -}); - -Deno.test({ - name: "test_str_ret#test", - fn: () => { - assertEquals(test_str_ret(), "🦕"); - }, -}); - -Deno.test({ - name: "test_hashmap#test", - fn: () => { - assertEquals(test_hashmap(), { my_map: { key: "value" } } as WithRecord); - }, -}); diff --git a/example/lib.rs b/example/lib.rs new file mode 100644 index 0000000..6620944 --- /dev/null +++ b/example/lib.rs @@ -0,0 +1,71 @@ +use deno_bindgen::deno_bindgen; + +#[deno_bindgen] +pub struct Context { + context: webusb::Context, +} + +#[deno_bindgen] +impl Context { + #[constructor] + pub fn init() -> Context { + let context = webusb::Context::init().expect("Unable to create context"); + Context { context } + } + + pub fn lsusb(&self) { + let devices = self.context.devices().expect("Unable to get devices"); + for device in devices { + if let Some(name) = device.product_name { + println!("Product Name: {}", name); + } + + println!("Vendor ID: {}", device.vendor_id); + println!("Product ID: {}\n", device.product_id); + } + } + + pub fn open(&mut self, vendor_id: u16, product_id: u16) -> Device { + let devices = self.context.devices().expect("Unable to get devices"); + let mut device = devices + .into_iter() + .find(|d| d.vendor_id == vendor_id && d.product_id == product_id) + .expect("Device not found."); + + device.open().expect("Unable to open device."); + + Device { device } + } +} + +#[deno_bindgen] +pub struct Device { + device: webusb::UsbDevice, +} + +impl Drop for Device { + fn drop(&mut self) { + self.device.close().expect("Unable to close device."); + } +} + +#[deno_bindgen] +impl Device { + pub fn claim_interface(&mut self, interface_number: u8) { + self + .device + .claim_interface(interface_number) + .expect("Unable to claim interface."); + } + + pub fn select_alternate_interface( + &mut self, + interface_number: u8, + alternate_setting: u8, + ) { + self + .device + .select_alternate_interface(interface_number, alternate_setting) + .expect("Unable to select alternate interface."); + } +} diff --git a/example/lsusb.ts b/example/lsusb.ts new file mode 100644 index 0000000..061c8c1 --- /dev/null +++ b/example/lsusb.ts @@ -0,0 +1,4 @@ +import { Context } from "./mod.ts"; + +const context = new Context(); +context.lsusb(); diff --git a/example/mod.ts b/example/mod.ts new file mode 100644 index 0000000..43aa009 --- /dev/null +++ b/example/mod.ts @@ -0,0 +1,203 @@ +// deno-lint-ignore-file + +// This file is automatically generated by deno_bindgen. +// Do not edit this file directly. + +const { dlopen } = Deno; + +const { symbols } = dlopen( + "/Users/divy/gh/deno_bindgen/example/target/debug/libdeno_usb.dylib", + { + __Context_init: { + parameters: [], + result: "pointer", + nonblocking: false, + }, + __Context_lsusb: { + parameters: [ + "pointer", + ], + result: "void", + nonblocking: false, + }, + __Context_open: { + parameters: [ + "pointer", + "u16", + "u16", + ], + result: "pointer", + nonblocking: false, + }, + __Context_dealloc: { + parameters: [ + "pointer", + ], + result: "void", + nonblocking: false, + }, + __Device_claim_interface: { + parameters: [ + "pointer", + "u8", + ], + result: "void", + nonblocking: false, + }, + __Device_select_alternate_interface: { + parameters: [ + "pointer", + "u8", + "u8", + ], + result: "void", + nonblocking: false, + }, + __Device_dealloc: { + parameters: [ + "pointer", + ], + result: "void", + nonblocking: false, + }, + }, +); + +function __Context_init(): Context { + const ret = symbols.__Context_init(); + return Context.__constructor(ret); +} + +function __Context_lsusb( + arg0: Deno.PointerObject | null, +): void { + return symbols.__Context_lsusb( + arg0, + ); +} + +function __Context_open( + arg0: Deno.PointerObject | null, + arg1: number, + arg2: number, +): Device { + const ret = symbols.__Context_open( + arg0, + arg1, + arg2, + ); + return Device.__constructor(ret); +} + +function __Context_dealloc( + arg0: Deno.PointerObject | null, +): void { + return symbols.__Context_dealloc( + arg0, + ); +} + +export class Context { + ptr: Deno.PointerObject | null = null; + + static __constructor(ptr: Deno.PointerObject | null) { + const self = Object.create(Context.prototype); + self.ptr = ptr; + return self; + } + + [Symbol.dispose]() { + this.dealloc(); + this.ptr = null; + } + + constructor() { + return __Context_init(); + } + + lsusb(): void { + return __Context_lsusb( + this.ptr, + ); + } + + open(arg0: number, arg1: number): Device { + return __Context_open( + this.ptr, + arg0, + arg1, + ); + } + + dealloc(): void { + return __Context_dealloc( + this.ptr, + ); + } +} + +function __Device_claim_interface( + arg0: Deno.PointerObject | null, + arg1: number, +): void { + return symbols.__Device_claim_interface( + arg0, + arg1, + ); +} + +function __Device_select_alternate_interface( + arg0: Deno.PointerObject | null, + arg1: number, + arg2: number, +): void { + return symbols.__Device_select_alternate_interface( + arg0, + arg1, + arg2, + ); +} + +function __Device_dealloc( + arg0: Deno.PointerObject | null, +): void { + return symbols.__Device_dealloc( + arg0, + ); +} + +export class Device { + ptr: Deno.PointerObject | null = null; + + static __constructor(ptr: Deno.PointerObject | null) { + const self = Object.create(Device.prototype); + self.ptr = ptr; + return self; + } + + [Symbol.dispose]() { + this.dealloc(); + this.ptr = null; + } + + claim_interface(arg0: number): void { + return __Device_claim_interface( + this.ptr, + arg0, + ); + } + + select_alternate_interface(arg0: number, arg1: number): void { + return __Device_select_alternate_interface( + this.ptr, + arg0, + arg1, + ); + } + + dealloc(): void { + return __Device_dealloc( + this.ptr, + ); + } +} diff --git a/example/src/lib.rs b/example/src/lib.rs deleted file mode 100644 index 75c0aca..0000000 --- a/example/src/lib.rs +++ /dev/null @@ -1,234 +0,0 @@ -use deno_bindgen::deno_bindgen; -use std::collections::HashMap; - -// Test "primitives" -#[deno_bindgen] -fn add(a: i32, b: i32) -> i32 { - a + b -} - -// Test Structs -#[deno_bindgen] -/// Doc comment for `Input` struct. -/// ...testing multiline -pub struct Input { - /// Doc comments get - /// transformed to JS doc - /// comments. - a: i32, - b: i32, -} - -#[deno_bindgen] -fn add2(input: Input) -> i32 { - input.a + input.b -} - -#[deno_bindgen] -fn add3(a: f32, b: f32) -> f32 { - a + b -} - -#[deno_bindgen] -fn add4(a: f64, b: f64) -> f64 { - a + b -} - -#[deno_bindgen] -fn add5(a: &[u8], b: &[u8]) -> Vec { - [a, b].concat() -} - -#[deno_bindgen] -fn add6(a: &[u8], b: &[u8]) -> Box<[u8]> { - [a, b].concat().into() -} - -// Test mixed types -#[deno_bindgen] -fn test_mixed(a: isize, b: Input) -> i32 { - a as i32 + b.a -} - -// Test mixed type codegen order -#[deno_bindgen] -fn test_mixed_order(a: i32, b: Input, c: i32) -> i32 { - a + b.a + c -} - -// Test serde support -#[deno_bindgen] -struct MyStruct { - arr: Vec, -} - -#[deno_bindgen] -fn test_serde(s: MyStruct) -> u8 { - if s.arr.contains(&"WORKS".to_string()) { - return 1; - } - 0 -} - -// Typescript codegen tests -#[deno_bindgen] -struct OptionStruct { - #[allow(dead_code)] - maybe: Option, -} - -// Test non_blocking -#[deno_bindgen(non_blocking)] -fn sleep(ms: u64) { - std::thread::sleep(std::time::Duration::from_millis(ms)); -} - -// Test other buffer dependent -// types. -#[deno_bindgen] -fn test_str(_s: &str) {} - -#[deno_bindgen] -fn test_buf(b: &[u8]) -> u8 { - b[0] -} - -#[deno_bindgen] -#[serde(rename_all = "lowercase")] -enum PlainEnum { - A { _a: String }, - B, - C, -} - -// Test mut buffer -#[deno_bindgen] -fn test_mut_buf(buf: &mut [u8]) { - buf[0] = 69; -} - -// Test mut buffer prevent return -// #[deno_bindgen] -// fn test_mut_buf_ret(buf: &mut [u8]) -> &mut [u8] { -// buf -// } - -// Test mut buffer musn't outlive symbol call -// #[deno_bindgen] -// fn test_mut_buf_outlive(_: &'static mut [u8]) { -// -// } - -#[deno_bindgen] -struct TestLifetimes<'l> { - text: &'l str, -} - -#[deno_bindgen] -enum TestLifetimeEnums<'a> { - Text { _text: &'a str }, -} - -#[deno_bindgen] -struct TestLifetimeWrap<'a> { - #[serde(borrow)] - _a: TestLifetimeEnums<'a>, -} - -#[deno_bindgen] -fn test_lifetime<'l>(s: TestLifetimes<'l>) -> usize { - s.text.len() -} - -#[deno_bindgen] -#[serde(tag = "key", content = "value")] -pub enum TagAndContent { - A { b: i32 }, - C { d: i32 }, -} - -#[deno_bindgen] -fn test_tag_and_content(arg: TagAndContent) -> i32 { - if let TagAndContent::A { b } = arg { - b - } else { - -1 - } -} - -#[deno_bindgen] -fn test_buffer_return(buf: &[u8]) -> &[u8] { - buf -} - -#[deno_bindgen(non_blocking)] -fn test_buffer_return_async(buf: &[u8]) -> &[u8] { - buf -} - -#[deno_bindgen] -fn test_manual_ptr() -> *const u8 { - let result = String::from("test").into_bytes(); - let length = (result.len() as u32).to_be_bytes(); - let mut v = length.to_vec(); - v.extend(result.clone()); - - let ret = v.as_ptr(); - // Leak the result to JS land. - ::std::mem::forget(v); - ret -} - -#[deno_bindgen(non_blocking)] -fn test_manual_ptr_async() -> *const u8 { - let result = String::from("test").into_bytes(); - let length = (result.len() as u32).to_be_bytes(); - let mut v = length.to_vec(); - v.extend(result.clone()); - - let ret = v.as_ptr(); - // Leak the result to JS land. - ::std::mem::forget(v); - ret -} - -#[deno_bindgen] -fn test_output() -> Input { - Input { a: 1, b: 2 } -} - -#[deno_bindgen(non_blocking)] -fn test_output_async() -> Input { - Input { a: 3, b: 4 } -} - -#[deno_bindgen] -struct TestReservedField { - r#type: u8, - r#ref: u8, -} - -#[deno_bindgen] -fn test_reserved_field() -> TestReservedField { - TestReservedField { - r#type: 1, - r#ref: 2, - } -} - -#[deno_bindgen] -fn test_str_ret() -> String { - String::from("🦕") -} - -#[deno_bindgen] -pub struct WithRecord { - my_map: HashMap, -} - -#[deno_bindgen] -fn test_hashmap() -> WithRecord { - let mut map = HashMap::new(); - map.insert("key".to_string(), "value".to_string()); - WithRecord { my_map: map } -}