diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c1e2c64 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..294d19e --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,23 @@ +name: Rust + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + - name: Build (--features full) + run: cargo build --verbose --features full + - name: Run tests (--features full) + run: cargo test --verbose --features full diff --git a/.github/workflows/update-flake-lock.yml b/.github/workflows/update-flake-lock.yml new file mode 100644 index 0000000..d87d158 --- /dev/null +++ b/.github/workflows/update-flake-lock.yml @@ -0,0 +1,21 @@ +name: update-flake-lock +on: + workflow_dispatch: # allows manual triggering + schedule: + - cron: '0 0 * * 0' # runs weekly on Sunday at 00:00 + +jobs: + lockfile: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v1 + - name: Update flake.lock + uses: DeterminateSystems/update-flake-lock@v19 + with: + pr-title: "Update flake.lock" # Title of PR to be created + pr-labels: | # Labels to be set on the PR + dependencies + automated diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da3abd5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.direnv +.DS_Store +src/HsBindgen.hs +target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..bc92b5d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,207 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "displaydoc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "hs-bindgen" +version = "0.9.0" +dependencies = [ + "hs-bindgen-attribute", + "hs-bindgen-traits", +] + +[[package]] +name = "hs-bindgen-attribute" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf0c7ca61a6f04e432eb50b569d5322a3a34ba4f61e6a84503d9a3f25e623098" +dependencies = [ + "displaydoc", + "hs-bindgen-types", + "lazy_static", + "quote", + "reflexive", + "rustc_version", + "semver 1.0.23", + "serde", + "syn 1.0.109", + "thiserror", + "toml", +] + +[[package]] +name = "hs-bindgen-traits" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac4ec8936241746802cfe99813764d0ab06f023a7350df3967e287be456f29fc" + +[[package]] +name = "hs-bindgen-types" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef6c1c28eb3c9a7fd73cb380e2a5114d511ef44b5ffaf5e76559669563abb05" +dependencies = [ + "cfg-if", + "displaydoc", + "proc-macro2", + "quote", + "thiserror", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "proc-macro2" +version = "1.0.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "reflexive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88ef05e7d63adad0f2c5f5873903226c03b7f27d219df7f37fb05ab98b5d420" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0ba9c3b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[package] +authors = ["Yvan Sraka "] +description = "Handy macro to generate C-FFI bindings to Rust for Haskell" +edition = "2021" +license = "MIT OR Apache-2.0" +name = "hs-bindgen" +repository = "https://github.com/yvan-sraka/hs-bindgen" +rust-version = "1.64.0" +version = "0.9.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] + +[dependencies] +hs-bindgen-attribute = { version = "0.9", default-features = false } +hs-bindgen-traits = { version = "0.9", default-features = false } + +[features] +default = ["std"] +full = ["reflexive", "std"] +reflexive = ["hs-bindgen-attribute/reflexive"] +std = ["hs-bindgen-traits/std"] diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..16fe87b --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..31aa793 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bd9592c --- /dev/null +++ b/README.md @@ -0,0 +1,96 @@ + + +# `hs-bindgen` + +Handy macro to generate C-FFI bindings to Rust for Haskell. + +This library intended to work best in a project configured by +[`cargo-cabal`](https://github.com/yvan-sraka/cargo-cabal). + +**N.B.** The MSRV is **1.64.0** since it use `core_ffi_c` feature. + +## Examples + +A minimal example would be to have a function annotated like this: + +```rust +use hs_bindgen::*; + +/// Haskell type signatures are auto-magically inferred from Rust function +/// types! This feature could slow down compilation, and be enabled with: +/// `hs-bindgen = { ..., features = [ "full" ] }` +#[hs_bindgen] +fn greetings(name: &str) { + println!("Hello, {name}!"); +} +``` + +This will be expanded to (you can try yourself with `cargo expand`): + +```rust +use hs_bindgen::*; + +fn greetings(name: &str) { + println!("Hello, {name}!"); +} + +#[no_mangle] // Mangling makes symbol names more difficult to predict. + // We disable it to ensure that the resulting symbol is really `__c_greetings`. +extern "C" fn __c_greetings(__0: *const core::ffi::c_char) -> () { + // `traits` module is `hs-bindgen::hs-bindgen-traits` + // n.b. do not forget to import it, e.g., with `use hs-bindgen::*` + traits::FromReprC::from(greetings(traits::FromReprRust::from(__0),)) +} +``` + +A more complete example, that use `borsh` to serialize ADT from Rust to Haskell +can be found [here](https://github.com/yvan-sraka/hs-bindgen-borsh-example). + +## Design + +First, I would thank [Michael Gattozzi](https://twitter.com/mgattozzi) who +implement [a (no longer maintained) implementation](https://github.com/mgattozzi/curryrs) +to binding generation between Rust and Haskell and +[his writings](https://blog.mgattozzi.dev/haskell-rust/) and guidance +really help me to quick start this project. + +I try to architect `hs-bindgen` with these core design principles: + +- **Simplicity:** as KISS UNIX philosophy of minimalism, meaning here I + tried to never re-implement feature already handled by Rust programming + language (parsing code, infer types, etc.), I rather rely on capabilities + of macro and trait systems. E.g. the only bit of parsing left in this + code its Haskell function signature (which is trivial giving the feature + set of authorized C-FFI safe types) ; + +- **Modularity:** this library is design in mind to work in a broader range + of usage, so this library should work in `#[no_std]` setting and most + features could be opt-out. E.g. the type inference offered by + [`antlion`](https://github.com/yvan-sraka/antlion) library is optional ; + +- **Stability:** this library implements no trick outside the scope of + stable C ABI (with well-defined memory layout convention), and ensure to + provide ergonomics without breaking this safety rule of thumb. There is + no magic that could be break by any `rustc` or GHC update! + +## Acknowledgments + +⚠️ This is still a working experiment, not yet production ready. + +`hs-bindgen` was heavily inspired by other interoperability initiatives, as +[`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) and +[`PyO3`](https://github.com/PyO3/pyo3). + +This project was part of a work assignment as an +[IOG](https://github.com/input-output-hk) contractor. + +## License + +Licensed under either of [Apache License](LICENSE-APACHE), Version 2.0 or +[MIT license](LICENSE-MIT) at your option. + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in this project by you, as defined in the Apache-2.0 license, +shall be dual licensed as above, without any additional terms or conditions. + + diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..39bacff --- /dev/null +++ b/default.nix @@ -0,0 +1,7 @@ +(import ( + fetchTarball { + url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz"; + sha256 = "0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2"; } +) { + src = ./.; +}).defaultNix diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..41a324c --- /dev/null +++ b/flake.lock @@ -0,0 +1,130 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1705309234, + "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1716509168, + "narHash": "sha256-4zSIhSRRIoEBwjbPm3YiGtbd8HDWzFxJjw5DYSDy1n8=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "bfb7a882678e518398ce9a31a881538679f6f092", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1706487304, + "narHash": "sha256-LE8lVX28MV2jWJsidW13D2qrHU/RUUONendL2Q/WlJg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "90f456026d284c22b3e3497be980b2e47d0b28ac", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1716776264, + "narHash": "sha256-fYzMk5o//g5Wt1g0FyOC8/XVllbGdVdzdylXxcanakU=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "8ef3f6a8f5af867ab5f75fc86fbd934a6351820b", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..f3b6228 --- /dev/null +++ b/flake.nix @@ -0,0 +1,17 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + rust-overlay.url = "github:oxalica/rust-overlay"; + flake-utils.url = "github:numtide/flake-utils"; + }; + outputs = { self, nixpkgs, rust-overlay, flake-utils, ... }: + flake-utils.lib.eachDefaultSystem (system: + let + overlays = [ (import rust-overlay) ]; + pkgs = import nixpkgs { inherit system overlays; }; + in with pkgs; { + devShells.default = mkShell { + buildInputs = [ (rust-bin.fromRustupToolchainFile ./rust-toolchain) ]; + }; + }); +} diff --git a/hs-bindgen-attribute/Cargo.lock b/hs-bindgen-attribute/Cargo.lock new file mode 100644 index 0000000..e8768fa --- /dev/null +++ b/hs-bindgen-attribute/Cargo.lock @@ -0,0 +1,191 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "displaydoc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "hs-bindgen-attribute" +version = "0.9.2" +dependencies = [ + "displaydoc", + "hs-bindgen-types", + "lazy_static", + "quote", + "reflexive", + "rustc_version", + "semver 1.0.23", + "serde", + "syn 1.0.109", + "thiserror", + "toml", +] + +[[package]] +name = "hs-bindgen-types" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef6c1c28eb3c9a7fd73cb380e2a5114d511ef44b5ffaf5e76559669563abb05" +dependencies = [ + "cfg-if", + "displaydoc", + "proc-macro2", + "quote", + "thiserror", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "proc-macro2" +version = "1.0.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "reflexive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88ef05e7d63adad0f2c5f5873903226c03b7f27d219df7f37fb05ab98b5d420" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/hs-bindgen-attribute/Cargo.toml b/hs-bindgen-attribute/Cargo.toml new file mode 100644 index 0000000..880e4c2 --- /dev/null +++ b/hs-bindgen-attribute/Cargo.toml @@ -0,0 +1,34 @@ +[package] +authors = ["Yvan Sraka "] +description = "Handy macro to generate C-FFI bindings from Rust to Haskell" +edition = "2021" +license = "MIT OR Apache-2.0" +name = "hs-bindgen-attribute" +repository = "https://github.com/yvan-sraka/hs-bindgen-attribute" +rust-version = "1.64.0" +version = "0.9.2" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +proc-macro = true + +[dependencies] +reflexive = { version = "0.4", optional = true } +displaydoc = "0.2" +hs-bindgen-types = "0.9" +lazy_static = { version = "1.4", optional = true } +quote = "1.0" +semver = "1.0" +serde = { version = "1.0", features = ["derive"] } +syn = { version = "1.0", features = [ "full" ] } +thiserror = "1.0" +toml = "0.5" + +[features] +default = [] +full = ["reflexive"] +reflexive = ["dep:reflexive", "dep:lazy_static"] + +[build-dependencies] +rustc_version = "0.2" diff --git a/hs-bindgen-attribute/README.md b/hs-bindgen-attribute/README.md new file mode 100644 index 0000000..87552bb --- /dev/null +++ b/hs-bindgen-attribute/README.md @@ -0,0 +1,24 @@ + + +# `hs-bindgen-attribute` + +This library define the `#[hs_bindgen]` procedural macro used by +[`hs-bindgen`](https://github.com/yvan-sraka/hs-bindgen) library. + +## Acknowledgments + +⚠️ This is still a working experiment, not yet production ready. + +This project was part of a work assignment as an +[IOG](https://github.com/input-output-hk) contractor. + +## License + +Licensed under either of [Apache License](LICENSE-APACHE), Version 2.0 or +[MIT license](LICENSE-MIT) at your option. + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in this project by you, as defined in the Apache-2.0 license, +shall be dual licensed as above, without any additional terms or conditions. + + diff --git a/hs-bindgen-attribute/build.rs b/hs-bindgen-attribute/build.rs new file mode 100644 index 0000000..3b9fbdb --- /dev/null +++ b/hs-bindgen-attribute/build.rs @@ -0,0 +1,9 @@ +//! Enable proc-macro diagnostics by default when toolchain is set on nightly! + +fn main() { + if let Ok(v) = rustc_version::version_meta() { + if v.channel == rustc_version::Channel::Nightly { + println!("cargo:rustc-cfg=DIAGNOSTICS"); + } + } +} diff --git a/hs-bindgen-attribute/hs-bindgen-types/Cargo.lock b/hs-bindgen-attribute/hs-bindgen-types/Cargo.lock new file mode 100644 index 0000000..31f06ab --- /dev/null +++ b/hs-bindgen-attribute/hs-bindgen-types/Cargo.lock @@ -0,0 +1,86 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "displaydoc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "hs-bindgen-types" +version = "0.9.1" +dependencies = [ + "cfg-if", + "displaydoc", + "proc-macro2", + "quote", + "thiserror", +] + +[[package]] +name = "proc-macro2" +version = "1.0.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/hs-bindgen-attribute/hs-bindgen-types/Cargo.toml b/hs-bindgen-attribute/hs-bindgen-types/Cargo.toml new file mode 100644 index 0000000..3104355 --- /dev/null +++ b/hs-bindgen-attribute/hs-bindgen-types/Cargo.toml @@ -0,0 +1,18 @@ +[package] +authors = ["Yvan Sraka "] +description = "Utility types behind hs-bindgen ergonomics" +edition = "2021" +license = "MIT OR Apache-2.0" +name = "hs-bindgen-types" +repository = "https://github.com/yvan-sraka/hs-bindgen-types" +rust-version = "1.64.0" +version = "0.9.1" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +cfg-if = "1.0.0" +displaydoc = "0.2" +proc-macro2 = "1.0" +quote = "1.0" +thiserror = "1.0" diff --git a/hs-bindgen-attribute/hs-bindgen-types/src/lib.rs b/hs-bindgen-attribute/hs-bindgen-types/src/lib.rs new file mode 100644 index 0000000..5bc1734 --- /dev/null +++ b/hs-bindgen-attribute/hs-bindgen-types/src/lib.rs @@ -0,0 +1,343 @@ +use cfg_if::cfg_if; +use core::ffi::*; +use displaydoc::Display; +use proc_macro2::TokenStream; +use quote::quote; +use thiserror::Error; + +/// Enumeration of all Haskell C-FFI safe types as the string representation of +/// their token in Haskell. +/// +/// FIXME: `Errno(c_int)` should be implemented as a Rust `enum` ... +/// https://hackage.haskell.org/package/base/docs/Foreign-C-Error.html +/// ... using `#[repr(i32)]` https://doc.rust-lang.org/nomicon/other-reprs.html +#[non_exhaustive] +pub enum HsType { + /// `Int32` + CInt, + /// `Int8` + CChar, + /// `Int8` + CSChar, + /// `Word8` + CUChar, + /// `Int16` + CShort, + /// `Word16` + CUShort, + /// `Word32` + CUInt, + /// `Int64` + CLong, + /// `Word64` + CULong, + /// `Int64` + CLLong, + /// `Word64` + CULLong, + /// `Word8` + CBool, + /// `Ptr CChar` + CString, + /// `Double` + CDouble, + /// `Float` + CFloat, + /// `()` + Empty, + /// `Ptr T` + Ptr(Box), + /// `IO T` + IO(Box), + /// FunPtr (S -> T) + FunPtr(Vec), +} + +impl std::fmt::Display for HsType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + HsType::CBool => "CBool".to_string(), + HsType::CChar => "CChar".to_string(), + HsType::CDouble => "CDouble".to_string(), + HsType::CFloat => "CFloat".to_string(), + HsType::CInt => "CInt".to_string(), + HsType::CLLong => "CLLong".to_string(), + HsType::CLong => "CLong".to_string(), + HsType::CSChar => "CSChar".to_string(), + HsType::CShort => "CShort".to_string(), + HsType::CString => "CString".to_string(), + HsType::CUChar => "CUChar".to_string(), + HsType::CUInt => "CUInt".to_string(), + HsType::CULLong => "CULLong".to_string(), + HsType::CULong => "CULong".to_string(), + HsType::CUShort => "CUShort".to_string(), + HsType::Empty => "()".to_string(), + HsType::Ptr(x) => format!("Ptr ({x})"), + HsType::IO(x) => format!("IO ({x})"), + HsType::FunPtr(types) => { + let args: Vec = types.iter().map(|arg| format!("{arg}")).collect(); + format!("FunPtr({})", args.join(" -> ")) + } + } + ) + } +} + +#[derive(Debug, Display, Error)] +pub enum Error { + /** type `{0}` isn't in the list of supported Haskell C-FFI types. + * Consider opening an issue https://github.com/yvan-sraka/hs-bindgen-types + * + * The list of available Haskell C-FFI types could be found here: + * https://hackage.haskell.org/package/base/docs/Foreign-C.html + */ + UnsupportedHsType(String), + /// found an open `(` without the matching closing `)` + UnmatchedParenthesis, + /// FunPtr is missing type parameter + FunPtrWithoutTypeArgument, +} + +pub struct ArrowIter<'a> { + remaining: &'a str, +} + +impl<'a> Iterator for ArrowIter<'a> { + type Item = &'a str; + + fn next(&mut self) -> Option { + let ArrowIter { remaining } = self; + + let mut open = 0; + let mut offset = 0; + + if remaining.trim().is_empty() { + return None; + } + + let mut matched: &str = ""; + + for c in remaining.chars() { + if c == '(' { + open += 1; + } else if c == ')' { + open -= 1; + } else if open == 0 && remaining[offset..].starts_with("->") { + matched = &remaining[..offset]; + offset += "->".len(); + break; + } + + offset += c.len_utf8(); + matched = &remaining[..offset]; + } + + *remaining = &remaining[offset..]; + Some(matched) + } +} + +impl<'a> From<&'a str> for ArrowIter<'a> { + fn from(value: &'a str) -> Self { + Self { remaining: value } + } +} + +impl std::str::FromStr for HsType { + type Err = Error; + + fn from_str(s: &str) -> Result { + let s = s.trim(); + if s == "()" { + Ok(HsType::Empty) + } else if !s.is_empty() && &s[..1] == "(" { + Ok(s[1..] + .strip_suffix(')') + .ok_or(Error::UnmatchedParenthesis)? + .parse()?) + } else if s.len() >= 2 && &s[..2] == "IO" { + Ok(HsType::IO(Box::new(s[2..].parse()?))) + } else if s.len() >= 3 && &s[..3] == "Ptr" { + Ok(HsType::Ptr(Box::new(s[3..].parse()?))) + } else if s.len() >= 6 && &s[..6] == "FunPtr" { + let mut s = s[6..].trim(); + + if let Some('(') = s.chars().next() { + s = s[1..] + .strip_suffix(')') + .ok_or(Error::UnmatchedParenthesis)?; + } + + let types: Vec<_> = ArrowIter { remaining: s } + .map(|s| s.parse::()) + .collect::>()?; + + if types.is_empty() { + return Err(Error::FunPtrWithoutTypeArgument); + } + + Ok(HsType::FunPtr(types)) + } else { + match s { + "CBool" => Ok(HsType::CBool), + "CChar" => Ok(HsType::CChar), + "CDouble" => Ok(HsType::CDouble), + "CFloat" => Ok(HsType::CFloat), + "CInt" => Ok(HsType::CInt), + "CLLong" => Ok(HsType::CLLong), + "CLong" => Ok(HsType::CLong), + "CSChar" => Ok(HsType::CSChar), + "CShort" => Ok(HsType::CShort), + "CString" => Ok(HsType::CString), + "CUChar" => Ok(HsType::CUChar), + "CUInt" => Ok(HsType::CUInt), + "CULLong" => Ok(HsType::CULLong), + "CULong" => Ok(HsType::CULong), + "CUShort" => Ok(HsType::CUShort), + ty => Err(Error::UnsupportedHsType(ty.to_string())), + } + } + } +} + +impl HsType { + /// Get the C-FFI Rust type that match the memory layout of a given HsType. + /// + /// This function return a `OUTPUT: proc_macro2::TokenStream` that should + /// be valid (considered as FFI-safe by `rustc`) in the context of a block + /// of form: `quote! { extern C fn _(_: #OUTPUT) {} }` + /// + /// c.f. https://doc.rust-lang.org/core/ffi/ + pub fn quote(&self) -> TokenStream { + match self { + // FIXME: add https://doc.rust-lang.org/core/ffi/enum.c_void.html + HsType::CBool => quote! { bool }, + HsType::CChar => quote! { core::ffi::c_char }, + HsType::CDouble => quote! { core::ffi::c_double }, + HsType::CFloat => quote! { core::ffi::c_float }, + HsType::CInt => quote! { core::ffi::c_int }, + HsType::CLLong => quote! { core::ffi::c_longlong }, + HsType::CLong => quote! { core::ffi::c_long }, + HsType::CSChar => quote! { core::ffi::c_schar }, + HsType::CShort => quote! { core::ffi::c_short }, + HsType::CString => HsType::Ptr(Box::new(HsType::CChar)).quote(), + HsType::CUChar => quote! { core::ffi::c_uchar }, + HsType::CUInt => quote! { core::ffi::c_uint }, + HsType::CULLong => quote! { core::ffi::c_ulonglong }, + HsType::CULong => quote! { core::ffi::c_ulong }, + HsType::CUShort => quote! { core::ffi::c_ushort }, + HsType::Empty => quote! { () }, + HsType::Ptr(x) => { + let ty = x.quote(); + quote! { *const #ty } + } + HsType::IO(x) => x.quote(), + HsType::FunPtr(types) => { + let ret = types.last().unwrap().quote(); + let args: Vec<_> = types[..types.len() - 1] + .iter() + .map(|arg| arg.quote()) + .collect(); + quote!(unsafe extern "C" fn(#(#args),*) -> #ret) + } + } + } +} + +/// Turn a given Rust type into his `HsType` target. +/// +/// Deducing what's the right Haskell type target given an arbitrary Rust type +/// is provided by `reflexive` feature of `hs-bingen-derive` and rely mostly on +/// Rust type inference through this trait. +pub trait ReprHs { + fn into() -> HsType; +} + +macro_rules! repr_hs { + ($($ty:ty => $ident:ident,)*) => {$( + impl ReprHs for $ty { + fn into() -> HsType { + HsType::$ident + } + } + )*}; +} +pub(crate) use repr_hs; + +repr_hs! { + c_char => CChar, + c_double => CDouble, + c_float => CFloat, + c_int => CInt, + c_short => CShort, + c_uchar => CUChar, + c_uint => CUInt, + c_ushort => CUShort, + () => Empty, +} + +cfg_if! { + if #[cfg(all(target_pointer_width = "64", not(windows)))] { + repr_hs! { + c_long => CLong, + c_ulong => CULong, + } + } else { + repr_hs! { + c_longlong => CLLong, + c_ulonglong => CULLong, + } + } +} + +impl ReprHs for *const T +where + T: ReprHs, +{ + fn into() -> HsType { + HsType::Ptr(Box::new(T::into())) + } +} + +impl ReprHs for *mut T +where + T: ReprHs, +{ + fn into() -> HsType { + HsType::Ptr(Box::new(T::into())) + } +} + +/* ********** Vector & Slices ********** */ + +impl ReprHs for Vec +where + T: ReprHs, +{ + fn into() -> HsType { + HsType::Ptr(Box::new(T::into())) + } +} + +impl ReprHs for &[T; N] +where + T: ReprHs, +{ + fn into() -> HsType { + HsType::Ptr(Box::new(T::into())) + } +} + +/* ********** Strings ********** */ + +use std::ffi::CString; + +repr_hs! { + CString => CString, + &CStr => CString, + String => CString, + &str => CString, +} diff --git a/hs-bindgen-attribute/src/haskell.rs b/hs-bindgen-attribute/src/haskell.rs new file mode 100644 index 0000000..613df7f --- /dev/null +++ b/hs-bindgen-attribute/src/haskell.rs @@ -0,0 +1,152 @@ +use displaydoc::Display; +use hs_bindgen_types::{ArrowIter, HsType}; +use std::str::FromStr; +use thiserror::Error; + +/// Produce the content of `lib/{module}.hs` given a list of Signature +pub(crate) fn template(module: &str, signatures: &[Signature]) -> String { + let modulename = module.replace("/", "."); + let names = signatures + .iter() + .map(|x| x.fn_name.clone()) + .collect::>() + .join(", "); + let imports = signatures + .iter() + .map(|sig| { + format!( + "foreign import ccall {} \"__c_{}\" {sig}", + if sig.fn_safe { + "safe" + } else { + warning(sig); + "unsafe" + }, + sig.fn_name + ) + }) + .collect::>() + .join("\n"); + format!( + "-- This file was generated by `hs-bindgen` crate and contains C FFI bindings +-- wrappers for every Rust function annotated with `#[hs_bindgen]` + +{{-# LANGUAGE ForeignFunctionInterface #-}} + +-- Why not rather using `{{-# LANGUAGE CApiFFI #-}}` language extension? +-- +-- * Because it's GHC specific and not part of the Haskell standard: +-- https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/ffi.html ; +-- +-- * Because the capabilities it gave (by rather works on top of symbols of a C +-- header file) can't work in our case. Maybe we want a future with an +-- {{-# LANGUAGE RustApiFFI #-}} language extension that would enable us to +-- work on top of a `.rs` source file (or a `.rlib`, but this is unlikely as +-- this format has purposely no public specification). + +{{-# OPTIONS_GHC -Wno-unused-imports #-}} + +module {modulename} ({names}) where + +import Data.Int +import Data.Word +import Foreign.C.String +import Foreign.C.Types +import Foreign.Ptr + +{imports}" + ) +} + +/// Warn user about what Haskell `unsafe` keyword does ... +pub(crate) fn warning(_sig: &Signature) { + #[cfg(DIAGNOSTICS)] + proc_macro::Diagnostic::spanned( + [proc_macro::Span::call_site()].as_ref(), + proc_macro::Level::Warning, + format!( + "Using: `foreign import ccall unsafe __c_, {} {_sig}` +means that Haskell Garbage-Collector will be locked during the foreign call. +/!\\ Do not use it for long computations in a multithreaded application or +it will slow down a lot your whole program ...", + _sig.fn_name + ), + ) + .emit(); +} + +#[derive(Display, Error, Debug)] +pub enum Error { + /** you should provide targeted Haskell type signature as attribute: + * `#[hs_bindgen(HS SIGNATURE)]` + */ + MissingSig, + /** given Haskell function definition is `{0}` but should have the form: + * `NAME :: TYPE` + * + * n.b. you can prefix function name like "unsafe NAME :: TYPE" and it will + * expand as: foreign import ccall unsafe __c_NAME NAME :: TYPE (knowing it + * default to foreign import ccall safe __c_NAME NAME :: TYPE ) ... + * ... /!\ Hope you know what you're doing! + */ + MalformedSig(String), + /// Haskell type error: {0} + HsType(String), +} + +/// Data structure that represent an Haskell function signature: +/// {fn_name} :: {fn_type[0]} -> {fn_type[1]} -> ... -> {fn_type[n-1]} +/// +/// FIXME: consider moving this struct and its traits' implementation into +/// `hs-bindgen-types` +pub(crate) struct Signature { + pub(crate) fn_name: String, + pub(crate) fn_safe: bool, + pub(crate) fn_type: Vec, +} + +impl std::fmt::Display for Signature { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{} :: {}", + self.fn_name, + self.fn_type + .iter() + .map(|x| x.to_string()) + .collect::>() + .join(" -> ") + ) + } +} + +impl FromStr for Signature { + type Err = Error; + + fn from_str(s: &str) -> Result { + (!s.is_empty()).then_some(()).ok_or(Error::MissingSig)?; + let mut x = s.split("::"); + let fn_name = x.next().ok_or(Error::MissingSig)?.trim(); + let fn_safe = !fn_name.starts_with("unsafe "); + let fn_name = if fn_safe { + fn_name.trim_start_matches("safe ") + } else { + fn_name.trim_start_matches("unsafe ") + } + .trim_start() + .to_string(); + + let fn_type = ArrowIter::from(x.next().ok_or_else(|| Error::MalformedSig(s.to_string()))?) + .map(|ty| { + ty.parse::() + .map_err(|ty| Error::HsType(ty.to_string())) + }) + .collect::, Error>>()?; + assert!(x.next().is_none(), "{}", Error::MalformedSig(s.to_string())); + Ok(Signature { + fn_name, + fn_safe, + fn_type, + }) + } +} diff --git a/hs-bindgen-attribute/src/lib.rs b/hs-bindgen-attribute/src/lib.rs new file mode 100644 index 0000000..ccba407 --- /dev/null +++ b/hs-bindgen-attribute/src/lib.rs @@ -0,0 +1,59 @@ +//! # `hs-bindgen-attribute` +//! +//! This library define the `#[hs_bindgen]` procedural macro used by +//! [`hs-bindgen`](https://github.com/yvan-sraka/hs-bindgen) library. +//! +//! ## Acknowledgments +//! +//! ⚠️ This is still a working experiment, not yet production ready. +//! +//! This project was part of a work assignment as an +//! [IOG](https://github.com/input-output-hk) contractor. +//! +//! ## License +//! +//! Licensed under either of [Apache License](LICENSE-APACHE), Version 2.0 or +//! [MIT license](LICENSE-MIT) at your option. +//! +//! Unless you explicitly state otherwise, any contribution intentionally submitted +//! for inclusion in this project by you, as defined in the Apache-2.0 license, +//! shall be dual licensed as above, without any additional terms or conditions. + +#![forbid(unsafe_code)] +#![cfg_attr(DIAGNOSTICS, feature(proc_macro_diagnostic))] + +use proc_macro::TokenStream; +use std::{fs, path::Path, sync::Mutex}; + +mod haskell; +mod reflexive; +mod rust; +mod toml; + +#[proc_macro_attribute] +pub fn hs_bindgen(attrs: TokenStream, input: TokenStream) -> TokenStream { + let mut output = input.clone(); + let item_fn: syn::ItemFn = syn::parse(input) + .expect("failed to parse as Rust code the content of `#[hs_bindgen]` macro"); + + // Generate extra Rust code that wrap our exposed function ... + let (signature, extern_c_wrapper) = rust::generate(attrs, item_fn); + + // Neat hack to keep track of all exposed functions ... + static SIGNATURES: Mutex> = Mutex::new(vec![]); + let signatures = &mut *SIGNATURES.lock().unwrap(); + signatures.push(signature); + + // Generate Haskell bindings into module defined in `hsbindgen.toml` config ... + let module = toml::config() + .default + .expect("your `hsbindgen.toml` file should contain a `default` field"); + let cargo_manifest_dir = std::env::var("CARGO_MANIFEST_DIR") + .expect("environment variable `CARGO_MANIFEST_DIR` must be set"); + let path = Path::new(&cargo_manifest_dir).join(format!("src/{}.hs", module)); + fs::write(&path, haskell::template(&module, signatures)) + .unwrap_or_else(|_| panic!("fail to write `{}` file", path.display())); + + output.extend(extern_c_wrapper); + output +} diff --git a/hs-bindgen-attribute/src/reflexive.rs b/hs-bindgen-attribute/src/reflexive.rs new file mode 100644 index 0000000..4f5a3d5 --- /dev/null +++ b/hs-bindgen-attribute/src/reflexive.rs @@ -0,0 +1,84 @@ +use crate::haskell; +#[cfg(feature = "reflexive")] +use hs_bindgen_types::HsType; + +#[cfg(feature = "reflexive")] +lazy_static::lazy_static! { + static ref SANDBOX: reflexive::Sandbox = + reflexive::Sandbox::new("hs-bindgen") + .unwrap() + .deps(&["hs-bindgen-types@0.8"]) + .unwrap() + ; +} + +/// Use Rust type inference (inside a `reflexive` sandbox) to deduce targeted +/// Haskell type signature that match a given `TokenStream` of a Rust `fn` +pub(crate) trait Eval { + fn from(_: T) -> Self; +} + +impl Eval<&syn::ItemFn> for haskell::Signature { + #[cfg(feature = "reflexive")] + fn from(item_fn: &syn::ItemFn) -> Self { + let fn_name = item_fn.sig.ident.to_string(); + let fn_safe = true; + let mut fn_type = vec![]; + for arg in &item_fn.sig.inputs { + fn_type.push(>::from(match arg { + syn::FnArg::Typed(p) => &p.ty, + _ => panic!("functions using `self` are not supported by `hs-bindgen`"), + })); + } + fn_type.push(HsType::IO(Box::new(match &item_fn.sig.output { + syn::ReturnType::Type(_, p) => >::from(p), + _ => HsType::Empty, + }))); + haskell::Signature { + fn_name, + fn_safe, + fn_type, + } + } + #[cfg(not(feature = "reflexive"))] + fn from(_: &syn::ItemFn) -> Self { + unreachable!() + } +} + +#[cfg(feature = "reflexive")] +impl Eval<&syn::Type> for HsType { + fn from(ty: &syn::Type) -> HsType { + use quote::quote; + SANDBOX + .eval(quote! { + <#ty as hs_bindgen_types::ReprHs>::into() + }) + .unwrap_or_else(|_| { + panic!( + "type `{}` doesn't implement `ReprHs` trait +consider opening an issue https://github.com/yvan-sraka/hs_bindgen_types + +n.b. if you trying to use a custom defined type, you need to specify the +Haskell type signature of your binding: #[hs_bindgen(HASKELL TYPE SIGNATURE)]", + quote! { #ty } + ) + }) + } +} + +/// Warn user about the build-time cost of relying on `reflexive` ... +/// +/// n.b. proc-macro diagnostics require nightly `proc_macro_diagnostic` feature +pub(crate) fn warning(_sig: &haskell::Signature) { + #[cfg(DIAGNOSTICS)] + proc_macro::Diagnostic::spanned( + [proc_macro::Span::call_site()].as_ref(), + proc_macro::Level::Warning, + format!( + "Implicit Haskell signature declaration could slow down compilation, +rather derive it as: #[hs_bindgen({_sig})]" + ), + ) + .emit(); +} diff --git a/hs-bindgen-attribute/src/rust.rs b/hs-bindgen-attribute/src/rust.rs new file mode 100644 index 0000000..244bfed --- /dev/null +++ b/hs-bindgen-attribute/src/rust.rs @@ -0,0 +1,65 @@ +use crate::{haskell, reflexive}; +use hs_bindgen_types::HsType; +use proc_macro::TokenStream; +use quote::{format_ident, quote}; + +/// Generate extra Rust code that wrap our exposed function +pub(crate) fn generate( + attrs: TokenStream, + item_fn: syn::ItemFn, +) -> (haskell::Signature, TokenStream) { + let rust_fn = format_ident!("{}", item_fn.sig.ident.to_string()); + + // Parse targeted Haskell function signature either from proc macro + // attributes or either from types from Rust `fn` item (using feature + // `reflexive` which is enabled by default) ... + let mut sig = { + let s = attrs.to_string(); + if cfg!(feature = "reflexive") && s.is_empty() { + let sig = >::from(&item_fn); + reflexive::warning(&sig); + sig + } else { + s.parse().unwrap_or_else(|e| panic!("{e}")) + } + }; + + // Ensure that signature not contain too much args ... + if sig.fn_type.len() > 8 { + panic!( + "Too many arguments! GHC C-ABI implementation does not currently behave well \ +with function with more than 8 arguments on platforms apart from x86_64 ..." + ) + } + + let ret = match sig.fn_type.pop().unwrap_or(HsType::Empty) { + HsType::IO(x) => x, + x => Box::new(x), + }; + + // Iterate through function argument types ... + let mut c_fn_args = quote! {}; + let mut rust_fn_values = quote! {}; + for (i, hs_c_ffi_type) in sig.fn_type.iter().enumerate() { + let arg = format_ident!("__{i}"); + let c_ffi_safe_type = hs_c_ffi_type.quote(); + c_fn_args.extend(quote! { #arg: #c_ffi_safe_type, }); + rust_fn_values.extend(quote! { traits::FromReprRust::from(#arg), }); + } + + // Generate C-FFI wrapper of Rust function ... + let c_fn = format_ident!("__c_{}", sig.fn_name); + let c_ret = ret.quote(); + let extern_c_wrapper = quote! { + #[no_mangle] // Mangling makes symbol names more difficult to predict. + // We disable it to ensure that the resulting symbol is really `#c_fn`. + extern "C" fn #c_fn(#c_fn_args) -> #c_ret { + // `traits` module is `hs-bindgen::hs-bindgen-traits` + // n.b. do not forget to import it, e.g., with `use hs-bindgen::*` + traits::FromReprC::from(#rust_fn(#rust_fn_values)) + } + }; + + sig.fn_type.push(HsType::IO(ret)); + (sig, extern_c_wrapper.into()) +} diff --git a/hs-bindgen-attribute/src/toml.rs b/hs-bindgen-attribute/src/toml.rs new file mode 100644 index 0000000..85a6b38 --- /dev/null +++ b/hs-bindgen-attribute/src/toml.rs @@ -0,0 +1,39 @@ +use semver::{Version, VersionReq}; +use serde::Deserialize; +use std::{env, fs, path::Path}; + +/// Struct that map the content of `hsbindgen.toml` config file +#[derive(Deserialize)] +pub(crate) struct Config { + pub(crate) default: Option, + pub(crate) version: Option, +} + +/// Read `hsbindgen.toml` config file generated by `cargo-cabal` +pub(crate) fn config() -> Config { + let cargo_manifest_dir = env::var("CARGO_MANIFEST_DIR") + .expect("environment variable `CARGO_MANIFEST_DIR` must be set"); + let cfg_path = Path::new(&cargo_manifest_dir).join("hsbindgen.toml"); + let cfg = fs::read_to_string(cfg_path).expect( + "fail to read content of `hsbindgen.toml` configuration file +n.b. you have to run the command `cargo-cabal` to generate it", + ); + let cfg = toml::from_str(&cfg).expect("fail to parse TOML content of `hsbindgen.toml` file"); + check_version(&cfg); + cfg +} + +/// Compatibility constraints on `cargo-cabal` version used +fn check_version(config: &Config) { + let req = VersionReq::parse("<=0.8").unwrap(); + let version = config + .version + .as_ref() + .expect("a version field is required in `hsbindgen.toml`"); + let version = Version::parse(version) + .expect("version field of `hsbindgen.toml` does not follow SemVer format"); + assert!( + req.matches(&version), + "incompatible versions of `cargo-cabal`/`hs-bindgen` used, please update" + ); +} diff --git a/hs-bindgen-traits/Cargo.lock b/hs-bindgen-traits/Cargo.lock new file mode 100644 index 0000000..176815d --- /dev/null +++ b/hs-bindgen-traits/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "hs-bindgen-traits" +version = "0.9.0" diff --git a/hs-bindgen-traits/Cargo.toml b/hs-bindgen-traits/Cargo.toml new file mode 100644 index 0000000..3433dbe --- /dev/null +++ b/hs-bindgen-traits/Cargo.toml @@ -0,0 +1,17 @@ +[package] +authors = ["Yvan Sraka "] +description = "Utility traits behind hs-bindgen ergonomics" +edition = "2021" +license = "MIT OR Apache-2.0" +name = "hs-bindgen-traits" +repository = "https://github.com/yvan-sraka/hs-bindgen-traits" +rust-version = "1.64.0" +version = "0.9.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] + +[features] +default = ["std"] +std = [] diff --git a/hs-bindgen-traits/README.md b/hs-bindgen-traits/README.md new file mode 100644 index 0000000..3361889 --- /dev/null +++ b/hs-bindgen-traits/README.md @@ -0,0 +1,34 @@ + + +# `hs-bingen-traits` + +Utility traits behind [`hs-bindgen`](https://github.com/yvan-sraka/hs-bindgen) +ergonomics. It helps user to easily define wrapper function to derive a Rust +type from and into a C-FFI safe target type (that match the memory layout of +an Haskell type). + +## What's this library for? + +[Does `repr(C)` define a trait I can use to check structs were declared with `#repr(C)`?](https://users.rust-lang.org/t/16323) +The answer is sadly no ... that's what this library trying to provide, like +what [`safer_ffi`](https://docs.rs/safer-ffi/latest/safer_ffi/layout/trait.ReprC.html) +does, but in a simpler and more minimal way, since the goal here is only to +target Haskell FFI. + +## Acknowledgments + +⚠️ This is still a working experiment, not yet production ready. + +This project was part of a work assignment as an +[IOG](https://github.com/input-output-hk) contractor. + +## License + +Licensed under either of [Apache License](LICENSE-APACHE), Version 2.0 or +[MIT license](LICENSE-MIT) at your option. + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in this project by you, as defined in the Apache-2.0 license, +shall be dual licensed as above, without any additional terms or conditions. + + diff --git a/hs-bindgen-traits/src/fun.rs b/hs-bindgen-traits/src/fun.rs new file mode 100644 index 0000000..2aa7637 --- /dev/null +++ b/hs-bindgen-traits/src/fun.rs @@ -0,0 +1,29 @@ +use crate::{private, FromReprRust}; + +macro_rules! repr_rust_fn { + () => { + impl FromReprRust Output> for Box Output> + where + Output: private::CFFISafe + 'static, + { + fn from(f: unsafe extern "C" fn() -> Output) -> Self { + unsafe { Box::new(move || f())} + } + } + }; + ($x:ident, $y:ident $(,$xs:ident, $ys: ident)*) => { + repr_rust_fn!($( $xs, $ys ),*); + impl<$x, $($xs,)* Output> FromReprRust Output> for Box Output> + where + Output: private::CFFISafe + 'static, + $x: private::CFFISafe + 'static$(, + $xs: private::CFFISafe + 'static)* + { + fn from(f: unsafe extern "C" fn($x $(, $xs )*) -> Output) -> Self { + unsafe { Box::new(move |$y $(,$ys)*| f($y $(,$ys)*))} + } + } + }; +} + +repr_rust_fn!(A, a, B, b, C, c, D, d, E, e, F, f); diff --git a/hs-bindgen-traits/src/lib.rs b/hs-bindgen-traits/src/lib.rs new file mode 100644 index 0000000..0cb7038 --- /dev/null +++ b/hs-bindgen-traits/src/lib.rs @@ -0,0 +1,165 @@ +//! # `hs-bingen-traits` +//! +//! Utility traits behind [`hs-bindgen`](https://github.com/yvan-sraka/hs-bindgen) +//! ergonomics. It helps user to easily define wrapper function to derive a Rust +//! type from and into a C-FFI safe target type (that match the memory layout of +//! an Haskell type). +//! +//! ## What's this library for? +//! +//! [Does `repr(C)` define a trait I can use to check structs were declared with `#repr(C)`?](https://users.rust-lang.org/t/16323) +//! The answer is sadly no ... that's what this library trying to provide, like +//! what [`safer_ffi`](https://docs.rs/safer-ffi/latest/safer_ffi/layout/trait.ReprC.html) +//! does, but in a simpler and more minimal way, since the goal here is only to +//! target Haskell FFI. +//! +//! ## Acknowledgments +//! +//! ⚠️ This is still a working experiment, not yet production ready. +//! +//! This project was part of a work assignment as an +//! [IOG](https://github.com/input-output-hk) contractor. +//! +//! ## License +//! +//! Licensed under either of [Apache License](LICENSE-APACHE), Version 2.0 or +//! [MIT license](LICENSE-MIT) at your option. +//! +//! Unless you explicitly state otherwise, any contribution intentionally submitted +//! for inclusion in this project by you, as defined in the Apache-2.0 license, +//! shall be dual licensed as above, without any additional terms or conditions. + +#![cfg_attr(not(feature = "std"), no_std)] +#![cfg_attr(not(feature = "std"), forbid(unsafe_code))] + +#[cfg(feature = "std")] +mod fun; +#[cfg(feature = "std")] +mod str; +#[cfg(feature = "std")] +mod vec; + +/// Generate C-FFI cast from a given Rust type. +/// +/// `impl FromReprC for Bar` -> means `from` Rust `Foo` type into C `Bar` repr +pub trait FromReprC: private::CFFISafe { + #[must_use] + fn from(_: T) -> Self; +} + +/// `impl IntoReprC for Bar` -> means `from` C `Foo` type into Rust `Bar` repr +pub trait IntoReprC { + #[must_use] + fn into(self) -> T; +} + +impl IntoReprC for T +where + U: FromReprC, + T: private::CFFISafe, +{ + #[inline] + fn into(self) -> U { + U::from(self) + } +} + +/// Generate safe Rust wrapper from a given C-FFI type. +/// +/// `impl FromReprRust for Bar` -> means `from` C `Foo` type into Rust `Bar` repr +pub trait FromReprRust { + #[must_use] + fn from(_: T) -> Self; +} + +/// `impl IntoReprRust for Bar` -> means `from` Rust `Foo` type into C `Bar` repr +pub trait IntoReprRust { + #[must_use] + fn into(self) -> T; +} + +impl IntoReprRust for T +where + U: FromReprRust, + T: private::CFFISafe, +{ + fn into(self) -> U { + U::from(self) + } +} + +mod private { + /// The trait `CFFISafe` is sealed and cannot be implemented for types outside this crate. + /// c.f. https://rust-lang.github.io/api-guidelines/future-proofing.html#c-sealed + pub trait CFFISafe {} + + macro_rules! c_ffi_safe { + ($($ty:ty),*) => {$( + impl CFFISafe for $ty {} + // `*const T` is C-FFI safe if `T` is C-FFI safe + impl CFFISafe for *const $ty {} + )*}; + } + + // C-FFI safe types (the previous macro avoid redundant code) + c_ffi_safe![(), i8, i16, i32, i64, u8, u16, u32, u64, f32, f64]; + + macro_rules! c_ffi_safe_fun { + () => { + impl CFFISafe for unsafe extern "C" fn() -> Output {} + }; + ($x:ident $(,$xs:ident)*) => { + c_ffi_safe_fun!($( $xs ),*); + impl<$x $(,$xs)*, Output> CFFISafe for unsafe extern "C" fn($x, $($xs),*) -> Output + where + Output: CFFISafe, + $x: CFFISafe, + $($xs: CFFISafe), + * {} + }; + } + + c_ffi_safe_fun!(A, B, C, D, E, F); +} + +macro_rules! transparent { + ($($ty:ty),*) => {$( + impl FromReprRust<$ty> for $ty { + #[inline] + fn from(x: $ty) -> Self { x } + } + impl FromReprC<$ty> for $ty { + #[inline] + fn from(x: $ty) -> Self { x } + } + + impl FromReprRust<*const $ty> for *const $ty { + #[inline] + fn from(x: *const $ty) -> Self { x } + } + impl FromReprC<*const $ty> for *const $ty { + #[inline] + fn from(x: *const $ty) -> Self { x } + } + )*}; +} + +// C-FFI safe type trivially implement both traits +transparent![i8, i16, i32, i64, u8, u16, u32, u64, f32, f64]; + +/// This is used by Rust function that doesn’t return any value +/// (`void` C equivalent). +impl FromReprC<()> for () { + #[inline] + fn from(_: ()) -> Self {} +} + +impl FromReprRust<*const T> for *mut T +where + *const T: private::CFFISafe, +{ + #[inline] + fn from(x: *const T) -> Self { + x as *mut T + } +} diff --git a/hs-bindgen-traits/src/str.rs b/hs-bindgen-traits/src/str.rs new file mode 100644 index 0000000..2a9811e --- /dev/null +++ b/hs-bindgen-traits/src/str.rs @@ -0,0 +1,63 @@ +//! This module defines convenient traits to let user-defined function take as +//! argument or return type either `CString`, `&CStr`, `String` or `&str` + +use crate::{FromReprC, FromReprRust}; +use std::ffi::{c_char, CStr, CString}; + +impl FromReprRust<*const c_char> for CString { + #[inline] + fn from(ptr: *const c_char) -> Self { + let r: &str = FromReprRust::from(ptr); + CString::new(r).unwrap() + } +} + +impl FromReprRust<*const c_char> for &CStr { + #[inline] + #[allow(clippy::not_unsafe_ptr_arg_deref)] + fn from(ptr: *const c_char) -> Self { + unsafe { CStr::from_ptr(ptr) } + } +} + +impl FromReprRust<*const c_char> for String { + #[inline] + fn from(ptr: *const c_char) -> Self { + let r: &str = FromReprRust::from(ptr); + r.to_string() + } +} + +impl FromReprRust<*const c_char> for &str { + #[inline] + fn from(ptr: *const c_char) -> Self { + let r: &CStr = FromReprRust::from(ptr); + r.to_str().unwrap() + } +} + +impl FromReprC for *const c_char { + #[inline] + fn from(s: CString) -> Self { + let x = s.as_ptr(); + // FIXME: this pattern is somehow duplicated in `vec` module and should + // rather live behind in a `AsPtr` trait, similar to the one defined by + // https://crates.io/crates/ptrplus + std::mem::forget(s); + x + } +} + +impl FromReprC for *const c_char { + #[inline] + fn from(s: String) -> Self { + FromReprC::from(CString::new(s).unwrap()) + } +} + +#[test] +fn _1() { + let x = "hello"; // FIXME: use Arbitrary crate + let y: &str = FromReprRust::from(FromReprC::from(x.to_string())); + assert!(x == y); +} diff --git a/hs-bindgen-traits/src/vec.rs b/hs-bindgen-traits/src/vec.rs new file mode 100644 index 0000000..64bd232 --- /dev/null +++ b/hs-bindgen-traits/src/vec.rs @@ -0,0 +1,46 @@ +use crate::{private, FromReprC, FromReprRust}; + +// FIXME: study what could be a good `Vec`/`&[T]` traits ergonomics ... +// n.b. the concept of `slice` have no C equivalent ... +// https://users.rust-lang.org/t/55118 + +impl FromReprRust<*const T> for &[T; N] +where + *const T: private::CFFISafe, +{ + #[inline] + #[allow(clippy::not_unsafe_ptr_arg_deref)] + fn from(ptr: *const T) -> Self { + let s = unsafe { std::slice::from_raw_parts(ptr, N) }; + s.try_into().unwrap_or_else(|_| { + let ty = std::any::type_name::(); + panic!("impossible to convert &[{ty}] into &[{ty}; {N}]"); + }) + } +} + +impl FromReprC> for *const T +where + *const T: private::CFFISafe, +{ + #[inline] + fn from(v: Vec) -> Self { + let x: *const T = v.as_ptr(); + // since the value is passed to Haskell runtime we want Rust to never + // drop it! + std::mem::forget(v); + // FIXME: I should double-check that this does not leak memory and + // that the value is well handled by GHC tracing Garbage Collector + x + // if not, we should export a utility function to let user drop + // the value, this technique was suggested e.g. here: + // https://stackoverflow.com/questions/39224904 + } +} + +#[test] +fn _1() { + let x = &[1, 2, 3]; // FIXME: use Arbitrary crate + let y: &[i32; 3] = FromReprRust::from(FromReprC::from(x.to_vec())); + assert!(x == y); +} diff --git a/hsbindgen.toml b/hsbindgen.toml new file mode 100644 index 0000000..f249b59 --- /dev/null +++ b/hsbindgen.toml @@ -0,0 +1,4 @@ +# This is required by doctests to works + +default = "HsBindgen" +version = "0.7.0" diff --git a/rust-toolchain b/rust-toolchain new file mode 100644 index 0000000..2bf5ad0 --- /dev/null +++ b/rust-toolchain @@ -0,0 +1 @@ +stable diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..b009291 --- /dev/null +++ b/shell.nix @@ -0,0 +1,4 @@ +(import (fetchTarball { + url = "https://github.com/edolstra/flake-compat/archive/b4a34015c698c7793d592d66adbab377907a2be8.tar.gz"; + sha256 = "1qc703yg0babixi6wshn5wm2kgl5y1drcswgszh4xxzbrwkk9sv7"; +}) { src = ./.; }).shellNix diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..be0c7eb --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,97 @@ +//! # `hs-bindgen` +//! +//! Handy macro to generate C-FFI bindings to Rust for Haskell. +//! +//! This library intended to work best in a project configured by +//! [`cargo-cabal`](https://github.com/yvan-sraka/cargo-cabal). +//! +//! **N.B.** The MSRV is **1.64.0** since it use `core_ffi_c` feature. +//! +//! ## Examples +//! +//! A minimal example would be to have a function annotated like this: +//! +//! ```rust +//! use hs_bindgen::*; +//! +//! /// Haskell type signatures are auto-magically inferred from Rust function +//! /// types! This feature could slow down compilation, and be enabled with: +//! /// `hs-bindgen = { ..., features = [ "full" ] }` +//! #[hs_bindgen] +//! fn greetings(name: &str) { +//! println!("Hello, {name}!"); +//! } +//! ``` +//! +//! This will be expanded to (you can try yourself with `cargo expand`): +//! +//! ```rust +//! use hs_bindgen::*; +//! +//! fn greetings(name: &str) { +//! println!("Hello, {name}!"); +//! } +//! +//! #[no_mangle] // Mangling makes symbol names more difficult to predict. +//! // We disable it to ensure that the resulting symbol is really `__c_greetings`. +//! extern "C" fn __c_greetings(__0: *const core::ffi::c_char) -> () { +//! // `traits` module is `hs-bindgen::hs-bindgen-traits` +//! // n.b. do not forget to import it, e.g., with `use hs-bindgen::*` +//! traits::FromReprC::from(greetings(traits::FromReprRust::from(__0),)) +//! } +//! ``` +//! +//! A more complete example, that use `borsh` to serialize ADT from Rust to Haskell +//! can be found [here](https://github.com/yvan-sraka/hs-bindgen-borsh-example). +//! +//! ## Design +//! +//! First, I would thank [Michael Gattozzi](https://twitter.com/mgattozzi) who +//! implement [a (no longer maintained) implementation](https://github.com/mgattozzi/curryrs) +//! to binding generation between Rust and Haskell and +//! [his writings](https://blog.mgattozzi.dev/haskell-rust/) and guidance +//! really help me to quick start this project. +//! +//! I try to architect `hs-bindgen` with these core design principles: +//! +//! - **Simplicity:** as KISS UNIX philosophy of minimalism, meaning here I +//! tried to never re-implement feature already handled by Rust programming +//! language (parsing code, infer types, etc.), I rather rely on capabilities +//! of macro and trait systems. E.g. the only bit of parsing left in this +//! code its Haskell function signature (which is trivial giving the feature +//! set of authorized C-FFI safe types) ; +//! +//! - **Modularity:** this library is design in mind to work in a broader range +//! of usage, so this library should work in `#[no_std]` setting and most +//! features could be opt-out. E.g. the type inference offered by +//! [`antlion`](https://github.com/yvan-sraka/antlion) library is optional ; +//! +//! - **Stability:** this library implements no trick outside the scope of +//! stable C ABI (with well-defined memory layout convention), and ensure to +//! provide ergonomics without breaking this safety rule of thumb. There is +//! no magic that could be break by any `rustc` or GHC update! +//! +//! ## Acknowledgments +//! +//! ⚠️ This is still a working experiment, not yet production ready. +//! +//! `hs-bindgen` was heavily inspired by other interoperability initiatives, as +//! [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) and +//! [`PyO3`](https://github.com/PyO3/pyo3). +//! +//! This project was part of a work assignment as an +//! [IOG](https://github.com/input-output-hk) contractor. +//! +//! ## License +//! +//! Licensed under either of [Apache License](LICENSE-APACHE), Version 2.0 or +//! [MIT license](LICENSE-MIT) at your option. +//! +//! Unless you explicitly state otherwise, any contribution intentionally submitted +//! for inclusion in this project by you, as defined in the Apache-2.0 license, +//! shall be dual licensed as above, without any additional terms or conditions. + +#![forbid(unsafe_code)] + +pub use hs_bindgen_attribute::hs_bindgen; +pub use hs_bindgen_traits as traits;