diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 4c9257c..0ad70f6 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -17,6 +17,10 @@ jobs: steps: - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: '16' + - name: Set up Python uses: actions/setup-python@v2 with: diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 4f43e42..10ece19 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -15,6 +15,10 @@ jobs: steps: - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: '16' + - name: Set up Python uses: actions/setup-python@v2 with: diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 4120bb1..e3fc163 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -12,12 +12,17 @@ jobs: build: runs-on: windows-2019 + if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')" env: VCVARS: C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat steps: - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: '16' + - name: Set up Python uses: actions/setup-python@v2 with: diff --git a/CMakeLists.txt b/CMakeLists.txt index 52acbb8..6dfdef3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.17) project( tolc - VERSION 0.2.0 + VERSION 0.3.0 LANGUAGES CXX) configure_file(docs/ReleaseNotes/version.in @@ -31,10 +31,15 @@ include(${modules}/Sanitizers.cmake) include(${modules}/StaticAnalyzers.cmake) include(${modules}/GetFrontend.cmake) -get_frontend(NAME Frontend.py VERSION v0.2.0) +get_frontend(NAME Frontend.py VERSION v0.3.0) copy_frontend_docs(NAME Frontend.py SRC_DIR ${frontend.py_SOURCE_DIR} COPY_TO ${CMAKE_CURRENT_LIST_DIR}/docs/packaging/docs/python) +get_frontend(NAME Frontend.wasm VERSION v0.4.1) +copy_frontend_docs( + NAME Frontend.wasm SRC_DIR ${frontend.wasm_SOURCE_DIR} COPY_TO + ${CMAKE_CURRENT_LIST_DIR}/docs/packaging/docs/webassembly) + include(${modules}/GetParser.cmake) get_parser(VERSION v0.2.0) @@ -105,8 +110,9 @@ add_library( target_include_directories(TolcInternal PUBLIC src) target_include_directories(TolcInternal SYSTEM PRIVATE ${cli11_content_SOURCE_DIR}/include) -target_link_libraries(TolcInternal PUBLIC Tolc::Parser Tolc::Frontend.py - spdlog::spdlog fmt::fmt) +target_link_libraries( + TolcInternal PUBLIC Tolc::Parser Tolc::Frontend.py Tolc::Frontend.wasm + spdlog::spdlog fmt::fmt) set_target_properties(TolcInternal PROPERTIES CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF) diff --git a/README.md b/README.md index 68296c4..48634ee 100644 --- a/README.md +++ b/README.md @@ -56,14 +56,14 @@ `Tolc` provides easy to use abstractions to create a bindings library directly from `CMake`: ```cmake -tolc_create_translation( +tolc_create_bindings( TARGET MyLib LANGUAGE python OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/python-bindings ) ``` -This will extract the public API from the target `MyLib`, give it to `Tolc` to create bindings, and expose it to `CMake` as the target `MyLib_python`. To see all options available for `tolc_create_translation`, please see the [the documentation](https://docs.tolc.io/cmake/reference/). +This will extract the public API from the target `MyLib`, give it to `Tolc` to create bindings, and expose it to `CMake` as the target `MyLib_python`. To see all options available for `tolc_create_bindings`, please see the [the documentation](https://docs.tolc.io/cmake/reference/). In this example you will find the built `CPython` library under `/tolc`, so you can use it straight away with: diff --git a/cmake/GetEmscripten.cmake b/cmake/GetEmscripten.cmake new file mode 100644 index 0000000..9a264ce --- /dev/null +++ b/cmake/GetEmscripten.cmake @@ -0,0 +1,48 @@ +include_guard() + +function(get_emscripten) + # Define the supported set of keywords + set(prefix ARG) + set(noValues) + set(singleValues VERSION) + set(multiValues) + # Process the arguments passed in can be used e.g. via ARG_NAME + cmake_parse_arguments(${prefix} "${noValues}" "${singleValues}" + "${multiValues}" ${ARGN}) + + if(NOT ARG_VERSION) + message( + FATAL_ERROR "Must provide a version. e.g. getEmscripten(VERSION 3.1.3)") + endif() + + # Download the SDK + set(emsdk_version ${ARG_VERSION}) + include(FetchContent) + FetchContent_Declare( + emsdk_entry + URL https://github.com/emscripten-core/emsdk/archive/refs/tags/${emsdk_version}.tar.gz + ) + + FetchContent_GetProperties(emsdk_entry) + if(NOT emscripten_entry_POPULATED) + FetchContent_Populate(emsdk_entry) + endif() + + set(sdkCommand ./emsdk) + if(${CMAKE_HOST_SYSTEM_NAME} STREQUAL Windows) + set(sdkCommand emsdk.bat) + endif() + + # Installs the Emscripten compiler + execute_process(COMMAND ${sdkCommand} install ${emsdk_version} + WORKING_DIRECTORY ${emsdk_entry_SOURCE_DIR}) + + # Writes the .emscripten file + execute_process(COMMAND ${sdkCommand} activate ${emsdk_version} + WORKING_DIRECTORY ${emsdk_entry_SOURCE_DIR}) + + # Export the variables + set(emsdk_SOURCE_DIR + ${emsdk_entry_SOURCE_DIR} + PARENT_SCOPE) +endfunction() diff --git a/cmake/packaging/scripts/gather_headers.py b/cmake/packaging/scripts/gather_headers.py index 8cf2ee5..a1cd42a 100644 --- a/cmake/packaging/scripts/gather_headers.py +++ b/cmake/packaging/scripts/gather_headers.py @@ -34,7 +34,6 @@ def parseArguments(): def main(): args = parseArguments() - print(args) # Make sure the directory of the output exists Path(args.combined_header).parent.mkdir(parents=True, exist_ok=True) diff --git a/cmake/packaging/tolc/tolcAddTarget.cmake b/cmake/packaging/tolc/tolcAddTarget.cmake deleted file mode 100644 index 9aabc9b..0000000 --- a/cmake/packaging/tolc/tolcAddTarget.cmake +++ /dev/null @@ -1,49 +0,0 @@ -include_guard() - -function(tolc_add_library) - # Define the supported set of keywords - set(prefix ARG) - set(noValues) - set(singleValues TARGET LANGUAGE) - set(multiValues INPUT) - # Process the arguments passed in - # can be used e.g. via ARG_TARGET - cmake_parse_arguments(${prefix} "${noValues}" "${singleValues}" - "${multiValues}" ${ARGN}) - - # Variables related to error messages: - # Cannot assume too new CMake version - set(function_name tolc_add_library) - set(usage - "Usage: ${function_name}(TARGET myLibrary LANGUAGE python INPUT src/tolcGeneratedFile.cpp src/myOwnExtension.cpp)" - ) - - # Helper function - function(error_with_usage msg) - message(FATAL_ERROR "Invalid call to ${function_name}. ${msg}\n${usage}") - endfunction() - - # Error checks on input - if(NOT ARG_TARGET) - error_with_usage( - "Missing TARGET argument. The name of the library.") - endif() - if(NOT ARG_LANGUAGE) - error_with_usage( - "Missing LANGUAGE argument. The language module to download. E.g. for 'python', this would automatically download pybind11.") - endif() - if(NOT ARG_INPUT) - error_with_usage( - "Missing INPUT argument. The source file(s) to be compiled.") - endif() - - if(${ARG_LANGUAGE} MATCHES "python") - # NOTE: Variable injected from tolcConfig file - get_pybind11(VERSION ${tolc_pybind11_version}) - # Create the python module - pybind11_add_module(${ARG_TARGET} ${ARG_INPUT} SYSTEM) - else() - error_with_usage( - "Unknown language input: ${ARG_LANGUAGE}. Valid input: [${tolc_supported_languages}]") - endif() -endfunction() diff --git a/cmake/packaging/tolc/tolcConfig.cmake b/cmake/packaging/tolc/tolcConfig.cmake index 6981078..703641b 100644 --- a/cmake/packaging/tolc/tolcConfig.cmake +++ b/cmake/packaging/tolc/tolcConfig.cmake @@ -26,11 +26,10 @@ find_tolc() message(STATUS "Using tolc executable: ${tolc_EXECUTABLE}") ### Config variables: These determine the behaviour of tolc ### -set(tolc_pybind11_version 2.8.1) +set(tolc_pybind11_version 2.9.1) # Comma separated list of supported languages -set(tolc_supported_languages "python") +set(tolc_supported_languages "python, wasm") include(${CMAKE_CURRENT_LIST_DIR}/tolcTranslate.cmake) include(${CMAKE_CURRENT_LIST_DIR}/GetPybind11.cmake) -include(${CMAKE_CURRENT_LIST_DIR}/tolcAddTarget.cmake) include(${CMAKE_CURRENT_LIST_DIR}/tolcCreateTranslation.cmake) diff --git a/cmake/packaging/tolc/tolcCreateTranslation.cmake b/cmake/packaging/tolc/tolcCreateTranslation.cmake index a6898fb..eff84ae 100644 --- a/cmake/packaging/tolc/tolcCreateTranslation.cmake +++ b/cmake/packaging/tolc/tolcCreateTranslation.cmake @@ -1,6 +1,6 @@ include_guard() -function(tolc_create_translation) +function(tolc_create_bindings) # Define the supported set of keywords set(prefix ARG) set(noValues DO_NOT_SEARCH_TARGET_INCLUDES NO_ANALYTICS) @@ -13,7 +13,7 @@ function(tolc_create_translation) # Variables related to error messages: # Cannot assume too new CMake version - set(function_name tolc_create_translation) + set(function_name tolc_create_bindings) set(usage "Usage: ${function_name}(TARGET myLibrary LANGUAGE python)") # Helper function @@ -48,11 +48,9 @@ function(tolc_create_translation) endif() # What the actual target name will be - set(tolcTargetName ${ARG_TARGET}_${ARG_LANGUAGE}) + set(tolc_target_name ${ARG_TARGET}_${ARG_LANGUAGE}) message( - STATUS - "Creating translation to language ${ARG_LANGUAGE} in target ${tolcTargetName}" - ) + STATUS "Creating bindings to ${ARG_LANGUAGE} in target ${tolc_target_name}") # Use the input target to create the translation tolc_translate_target( @@ -65,22 +63,45 @@ function(tolc_create_translation) ${ARG_LANGUAGE} OUTPUT ${ARG_OUTPUT}) - # Add a new target, representing the translation - # TODO: This should be changed when tolc can handle outputs explicitly (not just a directory, but a file) - tolc_add_library(TARGET ${tolcTargetName} LANGUAGE ${ARG_LANGUAGE} INPUT - ${ARG_OUTPUT}/${ARG_TARGET}.cpp) + + if(${ARG_LANGUAGE} MATCHES "python") + # NOTE: Variable injected from tolcConfig file + get_pybind11(VERSION ${tolc_pybind11_version}) + # Create the python module + pybind11_add_module(${tolc_target_name} ${ARG_OUTPUT}/${ARG_TARGET}.cpp + SYSTEM) + elseif(${ARG_LANGUAGE} MATCHES "wasm") + # Assumes that the Emscripten toolchain file is used + # Will result in a .js and a .wasm file + add_executable(${tolc_target_name} ${ARG_OUTPUT}/${ARG_TARGET}.cpp) + + # Export Promise as 'loadMyLib' for module 'MyLib' + # -s MODULARIZE=1 sets it as a promise based load + # Note that this is necessary for preJS to work properly + set_target_properties( + ${tolc_target_name} + PROPERTIES + LINK_FLAGS + "-s MODULARIZE=1 -s EXPORT_NAME=\"load${ARG_TARGET}\" --pre-js ${ARG_OUTPUT}/pre.js -lembind" + ) + else() + error_with_usage( + "Unknown language input: ${ARG_LANGUAGE}. Valid input: [${tolc_supported_languages}]" + ) + endif() # The added library target depends on the target being translated - add_dependencies(${tolcTargetName} tolc_translate_file_${ARG_TARGET}) + add_dependencies(${tolc_target_name} tolc_translate_file_${ARG_TARGET}) # NOTE: The user may need to provide additional links if they have their PUBLIC/PRIVATE dependencies missmatched - target_link_libraries(${tolcTargetName} PRIVATE ${ARG_TARGET}) + target_link_libraries(${tolc_target_name} PRIVATE ${ARG_TARGET}) set_target_properties( - ${tolcTargetName} + ${tolc_target_name} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/tolc" LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/tolc" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/tolc") # This allows the target to be called target_language, but still be imported e.g. in python as 'import target' - set_target_properties(${tolcTargetName} PROPERTIES OUTPUT_NAME ${ARG_TARGET}) + set_target_properties(${tolc_target_name} PROPERTIES OUTPUT_NAME + ${ARG_TARGET}) endfunction() diff --git a/cmake/packaging/tolc/tolcTranslate.cmake b/cmake/packaging/tolc/tolcTranslate.cmake index 731621b..dbcaa1d 100644 --- a/cmake/packaging/tolc/tolcTranslate.cmake +++ b/cmake/packaging/tolc/tolcTranslate.cmake @@ -5,7 +5,7 @@ function(tolc_translate_file) set(prefix ARG) set(noValues NO_ANALYTICS) set(singleValues INPUT LANGUAGE MODULE_NAME OUTPUT) - set(multiValues INCLUDES) + set(multiValues) # Process the arguments passed in # can be used e.g. via ARG_TARGET cmake_parse_arguments(${prefix} "${noValues}" "${singleValues}" @@ -14,7 +14,7 @@ function(tolc_translate_file) # Cannot assume too new CMake version set(function_name tolc_translate_file) set(usage - "Usage: ${function_name}(MODULE_NAME myLibrary LANGUAGE python INPUT include/myLibrary.hpp OUTPUT out [INCLUDES include])" + "Usage: ${function_name}(MODULE_NAME myLibrary LANGUAGE python INPUT include/myLibrary.hpp OUTPUT out)" ) # Helper function @@ -51,16 +51,6 @@ function(tolc_translate_file) set(noAnalytics "--no-analytics") endif() - # Turn the include directories to valid input flags - if(ARG_INCLUDES) - set(includes "") - foreach(include ${ARG_INCLUDES}) - list(APPEND includes -I ${include}) - endforeach() - else() - set(includes "") - endif() - set(command ${tolc_EXECUTABLE} ${ARG_LANGUAGE} @@ -70,10 +60,8 @@ function(tolc_translate_file) ${ARG_INPUT} --output ${ARG_OUTPUT} - ${includes} ${noAnalytics}) - # TODO: This should be changed when tolc can handle outputs explicitly (not just a directory, but a file) add_custom_target( tolc_translate_file_${ARG_MODULE_NAME} ALL WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} diff --git a/docs/ReleaseNotes/v0.3.0.md b/docs/ReleaseNotes/v0.3.0.md index ea5b303..d7f1b50 100644 --- a/docs/ReleaseNotes/v0.3.0.md +++ b/docs/ReleaseNotes/v0.3.0.md @@ -1 +1,11 @@ # News # + +## Features ## + +* Now supports generating bindings to WebAssembly via [`frontend.wasm`](https://github.com/Tolc-Software/frontend.wasm) + * Checkout the [documentation](https://docs.tolc.io) for more info. + + +## Breaking ## + +* Renamed `CMake` function `tolc_create_translation` to `tolc_create_bindings`. diff --git a/docs/packaging/docs/FAQ.md b/docs/packaging/docs/FAQ.md index be161dd..d6a793e 100644 --- a/docs/packaging/docs/FAQ.md +++ b/docs/packaging/docs/FAQ.md @@ -73,3 +73,15 @@ int const Example::i; For more information you can read about [static member declaration/instantiation on cppreference](https://en.cppreference.com/w/cpp/language/static), or [a discussion on this topic in the pybind repository](https://github.com/pybind/pybind11/issues/682). +## I get a link error while building `WebAssembly` on Windows with `Visual Studio` ## + +At the time of writing there is only experimental support for the `Visual Studio` generator for `Emscripten`. If you see error such as: + +```shell +LINK : warning LNK4044: unrecognized option '/-default-obj-ext'; ignored [MyProject.vcxproj] +LINK : fatal error LNK1104: cannot open file '.obj' [MyProject.vcxproj] +cl : command line warning D9002: ignoring unknown option '-g' [MyProject_wasm.vcxproj] myLib.cpp +```shell + +Then consider using the `Ninja` generator. It is the default on Windows with `Visual Studio`. + diff --git a/docs/packaging/docs/cmake/interface.md b/docs/packaging/docs/cmake/interface.md index e171288..61ab963 100644 --- a/docs/packaging/docs/cmake/interface.md +++ b/docs/packaging/docs/cmake/interface.md @@ -4,12 +4,12 @@ The `CMake` interface is used to gather information about the library you wish t ## Tolc CMake interface ## -The main interface is through `tolc_create_translation`. Example usage: +The main interface is through `tolc_create_bindings`. Example usage: ```cmake # This function comes from the tolc package itself # Creates the target example_python that can be imported and used from python -tolc_create_translation( +tolc_create_bindings( # Target to translate from TARGET example # Language to target @@ -21,7 +21,7 @@ tolc_create_translation( This assumes there is a `CMake` target called `example` that has some include directories marked either `PUBLIC` or `INTERFACE`. It will look through these directories for header files (any files ending in `.h` or `.hpp`) and export everything inside them to `python` via [`pybind11`](https://github.com/pybind/pybind11). It will then create the target `example_python` that can be used to compile an importable `CPython` library. The following figure shows the whole process: -![Tolc tolc_create_translation overview](../img/tolcCreateTranslationOverview.png "tolc_create_translation overview") +![Tolc tolc_create_bindings overview](../img/tolcCreateTranslationOverview.png "tolc_create_bindings overview") ## Complete example ## @@ -54,7 +54,7 @@ FetchContent_Declare( FetchContent_MakeAvailable(tolc_bootstrap) get_tolc() -tolc_create_translation( +tolc_create_bindings( TARGET Math LANGUAGE python OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/python-bindings diff --git a/docs/packaging/docs/cmake/reference.md b/docs/packaging/docs/cmake/reference.md index 321611e..387b898 100644 --- a/docs/packaging/docs/cmake/reference.md +++ b/docs/packaging/docs/cmake/reference.md @@ -1,8 +1,8 @@ # CMake Reference # -# `tolc_create_translation` # +# `tolc_create_bindings` # -Creates a target that is the library that can be consumed by some other language. +Creates a target that is the library that can be consumed by some other language. The resulting target name is `${TARGET}_${LANGUAGE}` (e.g. `MyCppLib_python`). ## Parameters ## @@ -35,7 +35,7 @@ Disable analytics. ## Example ## ```cmake -tolc_create_translation( +tolc_create_bindings( TARGET MyLib LANGUAGE python HEADERS diff --git a/docs/packaging/docs/guides/translating_a_cpp_library.md b/docs/packaging/docs/guides/translating_a_cpp_library.md index 88acb7a..1dc5fdd 100644 --- a/docs/packaging/docs/guides/translating_a_cpp_library.md +++ b/docs/packaging/docs/guides/translating_a_cpp_library.md @@ -56,11 +56,11 @@ find_package( REQUIRED) ``` -After the call to `find_package` we are free to use the `CMake` functions available in the `tolc` installation. To create bindings for `Math` we have to call the `tolc_create_translation` function +After the call to `find_package` we are free to use the `CMake` functions available in the `tolc` installation. To create bindings for `Math` we have to call the `tolc_create_bindings` function ```cmake # CMakeLists.txt -tolc_create_translation( +tolc_create_bindings( TARGET Math LANGUAGE python OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/python-bindings) diff --git a/docs/packaging/docs/index.md b/docs/packaging/docs/index.md index 1e1fd41..5c3df60 100644 --- a/docs/packaging/docs/index.md +++ b/docs/packaging/docs/index.md @@ -1,8 +1,14 @@ # Introduction # -`tolc` is a bindings compiler that makes code written in `C++` callable from other languages. `tolc` reads the header files of a project and automatically generates the correct bindings. +`Tolc` aims to make it as simple as possible to use `C++` from other languages. `Tolc` does not require you to change any part of your existing `C++` but simply reads your headers and automatically generates the correct bindings. -`tolc` is easily integrated into any `C++` project, either via the provided CMake Interface, or directly as a simple executable. +`Tolc` is easily integrated into any `C++` project, either via the provided CMake Interface, or directly as a simple executable. -`tolc` is available on Windows, MacOS, and Debian flavours of Linux. +`Tolc` is available on Windows, MacOS, and Debian flavours of Linux. +--- + +From here you can try any of the supported languages: + +* [Python](./python/introduction.md) +* [WebAssembly](./webassembly/introduction.md) diff --git a/docs/packaging/docs/usage.md b/docs/packaging/docs/usage.md index 4799b57..6754bf5 100644 --- a/docs/packaging/docs/usage.md +++ b/docs/packaging/docs/usage.md @@ -4,12 +4,12 @@ Given that you have installed `tolc` you should be able to use the `CMake` inter ## Via `CMake` -### `tolc_create_translation` +### `tolc_create_bindings` Example usage: ```CMake -tolc_create_translation( +tolc_create_bindings( TARGET MyLib LANGUAGE python OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/python-bindings @@ -25,5 +25,5 @@ An overview of the internal process is described below: 3. Download and make [`pybind11`](https://github.com/pybind/pybind11) available. 4. Create the target `MyLib_python` and link it to `pybind11` aswell as `MyLib`. -![Tolc tolc_create_translation overview](img/tolcCreateTranslationOverview.png "tolc_create_translation overview") +![Tolc tolc_create_bindings overview](img/tolcCreateTranslationOverview.png "tolc_create_bindings overview") diff --git a/docs/packaging/docs/webassembly/conversions.md b/docs/packaging/docs/webassembly/conversions.md new file mode 100644 index 0000000..1219b01 --- /dev/null +++ b/docs/packaging/docs/webassembly/conversions.md @@ -0,0 +1,108 @@ +# C++ to WebAssembly conversions # + +This page shows what is automatically translated and to what. +On the left is the `C++` and to the right what the corresponding interface in `javascript` will be. +Not converted means there will be no automatic translation to a corresponding `javascript` object. +Note that any restriction this poses only applies to the public interface of your code (e.g. your public headers). + +## Conversion tables ## + +| C++ | WebAssembly translation | +|:----------------------------------- |:------------------------------ | +| Namespace | Object namespace | +| Nested namespace | Nested object namespace | +| Class | Class | +| Public function | Class function | +| Private function | Not converted | +| Static member function | Static class function | +| Static member variable | Static member variable | +| Public const member variable | Read only property | +| Public non const member variable | Read write property | +| Private member variable | Not converted | +| Global variable | Module variable | +| Global static variable | Module variable | +| Free function | Module function | +| Overloaded function | Module function* | +| Enum | Enum | +| Scoped enum | Enum | +| Templated class/function | Not converted** | +| Specialized class template | Class*** | +| Specialized function template | Function**** | + +\* Changes the name of the function based on the arguments to avoid clashes. + +\*\* No direct translation to `javascript`. Will not emit warning. + +\*\*\* The naming convention for these classes can be found under the [Template Naming Convention page](template_naming_convention.md). + +\*\*\*\* Functions with different template arguments will behave as overloaded functions. + +| C++ Standard library class | WebAssembly translation | +|:------------------------------- |:----------------------------------------------------------------- | +| std::array | array | +| std::complex | ??? | +| std::deque | ??? | +| std::filesystem::path | ??? | +| std::forward\_list | Not converted | +| std::function | ??? | +| std::list | ??? | +| std::map | Map Object* | +| std::multimap | Not converted | +| std::multiset | Not converted | +| std::optional | ??? | +| std::pair | array | +| std::priority\_queue | Not converted | +| std::queue | Not converted | +| std::set | ??? | +| std::shared_ptr | ??? | +| std::stack | Not converted | +| std::tuple | ??? | +| std::unique_ptr | Value** | +| std::shared_ptr | Value | +| std::unordered\_map | ??? | +| std::unordered\_multimap | Not converted | +| std::unordered\_multiset | Not converted | +| std::unordered\_set | ??? | +| std::valarray | ??? | +| std::variant | ??? | +| std::vector | Vector Object*** | + +\* Converted via the [`register_map`](https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html#built-in-type-conversions) function (behaves like an `Object` in `javascript`). + +\*\* Note that arguments of type `unique_ptr` are not supported. For more info see [here](https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html#unique-ptr). + +\*\*\* Converted via the [`register_vector`](https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html#built-in-type-conversions) function (behaves like an `Object` in `javascript`). + + +| C++ builtin type | WebAssembly translation | +|:-------------------------- |:---------------------------------------------------------------- | +| bool | true || false | +| char | Number | +| char16\_t | Number | +| char32\_t | Number | +| double | Number | +| float | Number | +| int | Number | +| int8_t | Number | +| int16_t | Number | +| int32_t | Number | +| int64_t | Number | +| long double | Number | +| long int | Number | +| long long int | Number | +| short int | Number | +| signed char | Number | +| string | ArrayBuffer, Uint8Array, Uint8ClampedArray, Int8Array, or String | +| string\_view | String* | +| uint8_t | Number | +| uint16_t | Number | +| uint32_t | Number | +| uint64_t | Number | +| unsigned char | Number | +| unsigned int | Number | +| unsigned long int | Number | +| unsigned long long int | Number | +| unsigned short int | Number | +| wchar\_t | Number | + +\* Only works for globals (see [examples](./examples.md)) diff --git a/docs/packaging/docs/webassembly/examples.md b/docs/packaging/docs/webassembly/examples.md new file mode 100644 index 0000000..2da408b --- /dev/null +++ b/docs/packaging/docs/webassembly/examples.md @@ -0,0 +1,582 @@ +# Examples # + +Each example is taken from the test suite for `Tolc` and, given that you use the latest version, you can expect them all to work. + +To use `WebAssembly` from `javascript`, one has to load it in asynchronously. When using `Tolc` this is done with a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) on the `javascript` side. Each library named `MyLib` exports a `Promise` called `loadMyLib`, in every test the name is simply `m` for brevity. All tests use [`jest`](https://jestjs.io/), and the `javascript` test boiler plate is omitted: + +```javascript +var loadm = require('./build/m'); + +test('Tolc Test', () => { + loadm().then(m => { + // The actual javascript example body goes here + expect(m.sayTen()).toBe(10); + }); +}); +``` + + +## Classes ## + + +```cpp + +#include +#include + +class WithConstructor { +public: + explicit WithConstructor(std::string s) : m_s(s) {} + + // There is a separate .cpp file containing + // int const WithConstructor::i; + // To initialize i + static int const i = 5; + + std::string getS() { return m_s; } + + static int getStatic() { return 55; } + +private: + std::string m_s; +}; + +struct WithMembers { + int const i = 5; + std::string s = "hello"; +}; + +class WithFunction { +public: + int add(int i, int j) { + return i + j; + } +}; + +class WithPrivateFunction { + double multiply(double i, double j) { + return i * j; + } +}; + +namespace MyNamespace { + struct Nested { + int const i = 42; + }; +} + +struct WithEnum { + enum class Instrument { + Guitarr, + Flute + }; + Instrument i = Instrument::Flute; +}; + + + + +``` + + +```javascript + +// Statics are available without instantiation +// Static function +expect(m.WithConstructor.getStatic()).toBe(55); +// Static variable +expect(m.WithConstructor.i).toBe(5); + +var withConstructor = new m.WithConstructor("Hello"); +expect(withConstructor.getS()).toBe("Hello"); + +// Classes need to be deleted manually +withConstructor.delete(); + +// Const properties are read-only +var withMembers = new m.WithMembers(); +expect(withMembers.i).toBe(5); +try { + withMembers.i = 10; + expect(true).toBe(false); +} catch (err) { + expect(err.toString()).toMatch(/BindingError: WithMembers.i is a read-only property/i); +} +expect(withMembers.s).toBe("hello"); +withMembers.delete(); + +// Public functions are available +var withFunction = new m.WithFunction(); +expect(withFunction.add(5, 10)).toBe(15); +withFunction.delete(); + +// Cannot access private functions +var withPrivateFunction = new m.WithPrivateFunction(); +try { + withPrivateFunction.multiply(5, 10); + expect(true).toBe(false); +} catch (err) { + expect(err.toString()).toMatch(/TypeError: withPrivateFunction.multiply is not a function/i); +} +withPrivateFunction.delete(); + +// Classes can be found under their namespace +var nested = new m.MyNamespace.Nested(); +expect(nested.i).toBe(42); +nested.delete(); + +// Ok to nest Enums within classes +var withEnum = new m.WithEnum(); +expect(withEnum.i).toBe(m.WithEnum.Instrument.Flute); +withEnum.delete(); + +``` + + +## Enums ## + + +```cpp + +enum Unscoped { + Under, + Uboat, +}; + +enum class Scoped { + Sacred, + Snail, +}; + +class EnumTest { +public: + enum class Inside { + One, + Two + }; + + explicit EnumTest(Scoped s) : memberEnum(s), inside(Inside::One) {}; + + Inside inside; + + Scoped memberEnum; +}; + +Unscoped echo(Unscoped s) { + return s; +} + +namespace MyNamespace { + enum class Color { + Red, + Green, + Blue + }; + + struct Carrier { + enum class Translator { + Tolc + }; + }; +} + + +``` + + +```javascript + +// Can be passed as arguments +const snail = m.Scoped.Snail; +const enumTest = new m.EnumTest(snail); +expect(enumTest.memberEnum).toBe(snail); + +// Nested enums within classes +expect(enumTest.inside).toBe(m.EnumTest.Inside.One); +enumTest.delete(); + +// Unscoped enums work exactly the same +const uboat = m.Unscoped.Uboat; +expect(m.echo(uboat)).toBe(uboat); + +// Nested enums inside namespaces +const green = m.MyNamespace.Color.Green; +expect(green).toBe(m.MyNamespace.Color.Green); + +// Nested enums inside namespaces inside structs +const company = m.MyNamespace.Carrier.Translator.Tolc; +expect(company).toBe(m.MyNamespace.Carrier.Translator.Tolc); + +``` + + +## Functions ## + + +```cpp + +#include + +int sayTen() { + return 10; +} + +std::string giveBack(std::string const& s) { + return s; +} + +namespace MyNamespace { + int add(int x, int y) { + return x + y; + } + namespace Nested { + int increase(int x) { + return x + 1; + } + } +} + +``` + + +```javascript + +expect(m.sayTen()).toBe(10); + +expect(m.giveBack("hello")).toBe("hello"); + +// Nested functions are under their respective namespace +expect(m.MyNamespace.add(1, 2)).toBe(3); +expect(m.MyNamespace.Nested.increase(2)).toBe(3); + +``` + + +## Global Variables ## + + +```cpp + +#include + +int const i = 0; +double const d = 55; +std::string_view const stringView = "Hello world"; +const char* charPtr = "Hello world"; + +namespace MyNamespace { + int const i = 5; +} + +``` + + +```javascript + +expect(m.i).toBe(0); +expect(m.d).toBe(55); + +// Global strings of type std::string_view and const char* are converted +// Globals of type std::string has an open issue: +// https://github.com/emscripten-core/emscripten/issues/16275 +expect(m.stringView).toBe("Hello world"); +expect(m.charPtr).toBe("Hello world"); + +// Globals within namespaces work +expect(m.MyNamespace.i).toBe(5); + +``` + + +## Namespaces ## + + +```cpp + +#include + +namespace MyLib { + +int complexFunction() { + return 5; +} + + namespace We { + namespace Are { + namespace Going { + namespace Pretty { + namespace Deep { + std::string meaningOfLife() { + return "42"; + } + } + } + } + } + } +} + + +``` + + +```javascript + +expect(m.MyLib.complexFunction()).toBe(5); + +// Namespaces can be nested arbitrarily +expect(m.MyLib.We.Are.Going.Pretty.Deep.meaningOfLife()).toBe('42'); + +``` + + +## Smart Pointers ## + + +```cpp + +#include + +struct Example { + int m_value = 5; +}; + +struct ExampleShared { + int m_value = 10; +}; + +std::unique_ptr create_unique() { + return std::make_unique(); +} + +std::shared_ptr create_shared() { + return std::make_shared(); +} + +``` + + +```javascript + +// Note: Embind only supports *return*-values of std::unique_ptr +// An argument of type std::unique_ptr will return in an error message + +// std::unique_ptr just corresponds to the value +u = m.create_unique(); +expect(u.m_value).toBe(5); +u.delete(); + +// std::shared_ptr also just corresponds to the value +s = m.create_shared(); +expect(s.m_value).toBe(10); +s.delete(); + +``` + + +## std::array ## + + +```cpp + +#include +#include + +std::array getData3() { + return {0.0, 1.0, 2.0}; +} + +// Multiple array types at the same time +std::array getData2() { + return {0, 1}; +} + + +``` + + +```javascript + +var data3 = m.getData3(); + +// It's just a normal JS array +expect(data3.length).toBe(3); + +expect(data3).toStrictEqual([0, 1, 2]); + +var data2 = m.getData2(); +expect(data2.length).toBe(2); + +expect(data2).toStrictEqual([0, 1]); + +``` + + +## std::map ## + + +```cpp + +#include +#include + +std::map getData() { + std::map m; + m.insert({10, "hello"}); + return m; +} + + +``` + + +```javascript + +var data = m.getData(); + +expect(data.size()).toBe(1); + +expect(data.get(10)).toBe("hello"); + +data.set(50, "Stuff"); + +expect(data.size()).toBe(2); +expect(data.get(50)).toBe("Stuff"); + +``` + + +## std::pair ## + + +```cpp + +#include + +class MyClass { +public: + explicit MyClass(std::pair s) : m_s(s) {} + + std::pair getS() { return m_s; } + +private: + std::pair m_s; +}; + +class WithFunction { +public: + int sum(std::pair v) { + return v.first + v.second; + } +}; + + +``` + + +```javascript + +// On the javascript side, std::pair is a basic array +const myArray = ["hi", 4]; +withMember = new m.MyClass(myArray); +expect(withMember.getS()).toStrictEqual(myArray); +withMember.delete(); + +const withFunction = new m.WithFunction() +expect(withFunction.sum([1, 2])).toBe(3) +withFunction.delete(); + +``` + + +## std::tuple ## + + +```cpp + +#include +#include + +class MyClass { +public: + explicit MyClass(std::tuple _tuple) : m_tuple(_tuple) {} + + std::tuple getTuple() { return m_tuple; } + + std::tuple m_tuple; +}; + +class WithFunction { +public: + double sum(std::tuple t) { + return std::get<0>(t) + + std::get<1>(t) + + std::get<2>(t) + + std::get<3>(t); + } +}; + + +``` + + +```javascript + + +// Tuple converts from javascript array +const myArray = ["Hello World", 42]; +var myClass = new m.MyClass(myArray); +expect(myClass.getTuple()).toStrictEqual(myArray); + +// The array still need to match the underlying std::tuple structure +try { + // m_tuple is public + myClass.m_tuple = [1, 2, 3]; +} catch (err) { + expect(err.toString()).toMatch(/TypeError: Incorrect number of tuple elements for tuple_string_int: expected=2, actual=3/i); +} + +myClass.delete(); + +// Can handle different Number types +var withFunction = new m.WithFunction(); +expect(withFunction.sum([1, 2, 3.3, 2.0])).toBeCloseTo(8.3, 5); + +withFunction.delete(); + +``` + + +## std::vector ## + + +```cpp + +#include + +std::vector getData() { + return {0, 1, 2}; +} + + +``` + + +```javascript + +var data = m.getData(); + +expect(data.size()).toBe(3); + +for (var i = 0; i < data.size(); i++) { + expect(data.get(i)).toBe(i); +} + +data.push_back(3); + +expect(data.size()).toBe(4); + +expect(data.get(3)).toBe(3); + +``` + diff --git a/docs/packaging/docs/webassembly/faq.md b/docs/packaging/docs/webassembly/faq.md new file mode 100644 index 0000000..993a119 --- /dev/null +++ b/docs/packaging/docs/webassembly/faq.md @@ -0,0 +1,6 @@ +# FAQ # + +## A note on const within containers ## + +As of writing `Embind` does not support const within the template parameter list of most standard containers (e.g. `std::map` or `std::pair`). +These will be ignored from the output. diff --git a/docs/packaging/docs/webassembly/introduction.md b/docs/packaging/docs/webassembly/introduction.md new file mode 100644 index 0000000..a1c1db3 --- /dev/null +++ b/docs/packaging/docs/webassembly/introduction.md @@ -0,0 +1,107 @@ +# WebAssembly with Tolc # + +In order for `C++` to be called from `javascript` there has to be an interface level. `tolc` generates this level from your already written `C++` interface. +To be as close to what an engineer would have written, `tolc` generates human readable [`embind11`](https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html#embind). +This is then compiled to a `.wasm` and a `.js` file that `javascript` can import. + +## Using a `C++` library from `javascript` ## + +This is a quick guide to using a `C++` library (here called `MyLib`) from `javascript`. We will: + +1. Download and use `Tolc` +2. Download and set up `Emscripten` +3. Use the resulting `WebAssembly` from `javascript` + +The following works on all supported platforms. On all platforms you need `git` available in your `path`. Commands that should be run from a terminal starts with `$ `, while comments starts with `# `. + +### Downloading `Tolc` ### + +Just add the following in a `CMakeLists.txt` below where the library you intend to use from `javascript` is defined: + +```cmake +# Download Tolc +# Can be ["latest", "v0.2.0", ...] +set(tolc_version latest) +include(FetchContent) +FetchContent_Declare( + tolc_entry + URL https://github.com/Tolc-Software/tolc/releases/download/${tolc_version}/tolc-${CMAKE_HOST_SYSTEM_NAME}.tar.xz +) +FetchContent_Populate(tolc_entry) + +find_package( + tolc + CONFIG + PATHS + ${tolc_entry_SOURCE_DIR} + REQUIRED) + +tolc_create_bindings( + TARGET MyLib + LANGUAGE wasm + OUTPUT wasm-bindings) +``` + +Assuming your library is called `MyLib`, and the bindings should be generated to the directory `wasm-bindings`. + +### Downloading `Emscripten` ### + +In order to compile your library to `WebAssembly`, you need to download the [`Emscripten compiler`](https://emscripten.org/). This is typically done via the `Emscripten SDK`. Navigate to the directory where you want to install and run the following commands: + +```shell +# Download SDK +$ git clone https://github.com/emscripten-core/emsdk.git +$ cd emsdk +``` + +Now follow the specifig commands for your platform. + +#### Linux/MacOS #### + +From within the `emsdk` directory: + +```shell +# Download and install locally +$ ./emsdk install 3.1.3 +# Writes configuration file .emscripten +$ ./emsdk activate 3.1.3 +``` + +#### Windows #### + +From within the `emsdk` directory: + +```shell +# Download and install locally +$ emsdk.bat install 3.1.3 +# Writes configuration file .emscripten +$ emsdk.bat activate 3.1.3 +``` + +### Configuring Your Project ### + +Now when configuring your `CMake` project, pass the toolchain flag `-DCMAKE_TOOLCHAIN_FILE=${EMSDK_DIRECTORY}/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake`. Where you need to replace `${EMSDK_DIRECTORY}` with the directory of the previously downloaded `Emscripten SDK`. Note that the directory separator used by `CMake` is always forward slash (`/`), even on Windows. + +Example: + +```shell +# Configures project to build using Emscripten +$ cmake -S. -Bbuild -DCMAKE_TOOLCHAIN_FILE=${EMSDK_DIRECTORY}/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake +``` + +### Using From `javascript` ### + +Looking into `build/tolc` you should see `MyLib.js` aswell as `MyLib.wasm`. `MyLib.js` exports a `Promise` that loads the built `WebAssembly`. Here is an example usage: + +```javascript +// run.js +const loadMyLib = require('./build/MyLib'); + +loadMyLib().then(MyLib => { + // From here you can use the C++ functions of your library as usual + MyLib.myCppFunction(); +}); +``` + +If you want to see what more is supported you can take a look at [the Examples section](./examples.md). + diff --git a/docs/packaging/docs/webassembly/overloaded_functions_naming_convention.md b/docs/packaging/docs/webassembly/overloaded_functions_naming_convention.md new file mode 100644 index 0000000..e35a89b --- /dev/null +++ b/docs/packaging/docs/webassembly/overloaded_functions_naming_convention.md @@ -0,0 +1,26 @@ +# Overloaded Functions Naming Convention # + +When creating bindings for a function `f` `tolc` will choose a name based on if the function is overloaded or not. + +For example: + +```cpp +double f(); + +double f(int); +``` + +The two functions will be available from `javascript` as `f` and `f_int` respectively. + +```python +var loadMyLib = require('./myLib.js'); + +loadMyLib().then(MyLib => { + const result0 = MyLib.f(); + const result1 = MyLib.f_int(); +}); +``` + +Multiple arguments are added to the name as joined separated with an underscore (`_`). +The names are meant to be as predictable as possible. The rules are the same as for [the template naming conventions](./template_naming_convention.md). + diff --git a/docs/packaging/docs/webassembly/template_naming_convention.md b/docs/packaging/docs/webassembly/template_naming_convention.md new file mode 100644 index 0000000..279ea97 --- /dev/null +++ b/docs/packaging/docs/webassembly/template_naming_convention.md @@ -0,0 +1,90 @@ +# Template Naming Convention # + +When creating bindings for a templated `class` `tolc` will choose a name based on the template parameters. +For example: + +```cpp +template +class Example { +public: +T f(T type) { + return type; +} +}; + +template class Example; +``` + +The specialized `class` `Example` will be available from `javascript` as `Example_int`: + +```javascript +var loadMyLib = require('./build/MyLib'); + +loadm().then(MyLib => { + example = new MyLib.Example_int(); + // Prints 5 + print(example.f(5)) + example.delete(); +}); +``` + +Multiple template parameters are separated with an underscore (_). +The names are meant to be as predictable as possible. The rules are: + +* `std::` is removed from any standard library type. +* `_` is removed from any standard library type. +* User defined types are left untouched (i.e. the class `MyNamespace::MyClass` will result in appending `MyClass`). + +## Type to string conversions ## + +| C++ type | Resulting name | +|:------------------------------- |:------------------------ | +| std::array | array | +| std::complex | complex | +| std::deque | deque | +| std::filesystem::path | path | +| std::forward\_list | forwardlist | +| std::function | function | +| std::list | list | +| std::map | map | +| std::multimap | multimap | +| std::multiset | multiset | +| std::optional | optional | +| std::pair | pair | +| std::priority\_queue | priorityqueue | +| std::queue | queue | +| std::set | set | +| std::shared\_ptr | sharedptr | +| std::stack | stack | +| std::tuple | tuple | +| std::unique\_ptr | uniqueptr | +| std::unordered\_map | unorderedmap | +| std::unordered\_multimap | unorderedmultimap | +| std::unordered\_multiset | unorderedmultiset | +| std::unordered\_set | unorderedset | +| std::valarray | valarray | +| std::variant | variant | +| std::vector | vector | +| bool | bool | +| char | char | +| char16\_t | char16t | +| char32\_t | char32t | +| double | double | +| float | float | +| int | int | +| Integral | Integral literal* | +| long double | longdouble | +| long int | longint | +| long long int | longlongint | +| short int | shortint | +| signed char | signedchar | +| string | string | +| string\_view | stringview | +| unsigned char | unsignedchar | +| unsigned int | unsignedint | +| unsigned long int | unsignedlongint | +| unsigned long long int | unsignedlonglongint | +| unsigned short int | unsignedshortint | +| wchar\_t | wchart | + +\* For example the `3` in `MyClass>` results in `MyClass_array_int_3`. diff --git a/docs/packaging/mkdocs.yml b/docs/packaging/mkdocs.yml index 3ecad40..9102373 100644 --- a/docs/packaging/mkdocs.yml +++ b/docs/packaging/mkdocs.yml @@ -7,16 +7,20 @@ nav: - Introduction: 'index.md' - Installing: 'installing.md' - Usage: 'usage.md' - - Guides: - - Translating a C++ library: 'guides/translating_a_cpp_library.md' - - CMake: - - Interface: 'cmake/interface.md' - - Reference: 'cmake/reference.md' - Python Interface: - Introduction: 'python/introduction.md' - Examples: 'python/examples.md' - Conversions: 'python/conversions.md' - Template Naming Convention: 'python/template_naming_convention.md' + - WebAssembly Interface: + - Introduction: 'webassembly/introduction.md' + - Examples: 'webassembly/examples.md' + - Conversions: 'webassembly/conversions.md' + - Guides: + - Translating a C++ library: 'guides/translating_a_cpp_library.md' + - CMake: + - Interface: 'cmake/interface.md' + - Reference: 'cmake/reference.md' - FAQ: 'FAQ.md' markdown_extensions: - fenced_code diff --git a/src/CommandLine/parse.cpp b/src/CommandLine/parse.cpp index 2622269..7ba10f6 100644 --- a/src/CommandLine/parse.cpp +++ b/src/CommandLine/parse.cpp @@ -22,50 +22,66 @@ int parseInternal(CLI::App& app, } } // namespace -/** -* Adds the python subcommand with hooks to the rootApp -*/ -[[nodiscard]] CLI::App* addPythonCommands(CLI::App& rootApp, - CommandLine::CLIResult& result) { - auto* python = - rootApp.add_subcommand("python", "Translate input for use from python"); - - // Adding directly to python since these options +void addCommonCommands(CLI::App& languageApp, CommandLine::CLIResult& result) { + // Adding directly to languageApp since these options // should come after the language subcommand in the CLI - python - ->add_option("-i,--input", - result.inputFile, - "The interface file to be translated.") + languageApp + .add_option("-i,--input", + result.inputFile, + "The interface file to be translated.") ->required(); - // E.g. moduleName = defaultModule - // -> import defaultModule - // will be valid python - python - ->add_option("-m,--module-name", - result.moduleName, - "The name of the exported library.") + languageApp + .add_option("-m,--module-name", + result.moduleName, + "The name of the exported library.") ->required(); - python - ->add_option("-o,--output", - result.outputDirectory, - "The output directory where the bindings will be stored.") + languageApp + .add_option("-o,--output", + result.outputDirectory, + "The output directory where the bindings will be stored.") ->required(); - python->add_option( + languageApp.add_option( "-I", result.includes, "Path to search for when resolving #include statements."); - python->add_flag( + languageApp.add_flag( "--no-analytics", result.noAnalytics, "Don't gather analytics."); +} + +/** +* Adds the python subcommand with hooks to the rootApp +*/ +[[nodiscard]] CLI::App* addPythonCommands(CLI::App& rootApp, + CommandLine::CLIResult& result) { + auto* python = rootApp.add_subcommand( + "python", "Create bindings to use C++ from python via CPython"); + + addCommonCommands(*python, result); python->callback([&result]() { result.language = "python"; }); return python; } +/** +* Adds the wasm subcommand with hooks to the rootApp +*/ +[[nodiscard]] CLI::App* addWasmCommands(CLI::App& rootApp, + CommandLine::CLIResult& result) { + auto* wasm = rootApp.add_subcommand( + "wasm", "Create bindings to use C++ from javascript via WebAssembly"); + + addCommonCommands(*wasm, result); + + wasm->callback([&result]() { result.language = "wasm"; }); + + return wasm; +} + /** * Add options according to CLI11, and add hooks so that the values will be stored in result */ @@ -75,6 +91,7 @@ addSubcommandsAndOptions(CLI::App& app, CommandLine::CLIResult& result) { std::vector apps = {&app}; apps.push_back(addPythonCommands(app, result)); + apps.push_back(addWasmCommands(app, result)); // The number of subcommands added (as of writing only python => 1) app.require_subcommand(1); @@ -85,7 +102,7 @@ addSubcommandsAndOptions(CLI::App& app, CommandLine::CLIResult& result) { [[nodiscard]] std::optional parse(int argc, const char** argv) { CLI::App app { - "Tolc is an automatic bindings generator between C++ and other languages"}; + "Tolc is a bindings compiler between C++ and other languages"}; CommandLine::CLIResult result; auto apps = CommandLine::addSubcommandsAndOptions(app, result); diff --git a/src/TolcInternal/buildConfig.cpp b/src/TolcInternal/buildConfig.cpp index b685cb0..03f34de 100644 --- a/src/TolcInternal/buildConfig.cpp +++ b/src/TolcInternal/buildConfig.cpp @@ -34,6 +34,8 @@ std::optional buildConfig(CommandLine::CLIResult const& cli) { if (cli.language == "python") { config.language = Config::Language::Python; + } else if (cli.language == "wasm") { + config.language = Config::Language::Wasm; } else { spdlog::error("Unknonwn language: {}", cli.language); return {}; diff --git a/src/TolcInternal/buildConfig.hpp b/src/TolcInternal/buildConfig.hpp index 0f5f6d2..dd9aebc 100644 --- a/src/TolcInternal/buildConfig.hpp +++ b/src/TolcInternal/buildConfig.hpp @@ -12,7 +12,7 @@ namespace TolcInternal { * Stores everyting needed to use the Parser and Frontends */ struct Config { - enum class Language { Python }; + enum class Language { Python, Wasm }; // Tells the parser what to do Parser::Config parserConfig; diff --git a/src/TolcInternal/run.cpp b/src/TolcInternal/run.cpp index 2c04723..d08bb9e 100644 --- a/src/TolcInternal/run.cpp +++ b/src/TolcInternal/run.cpp @@ -1,15 +1,15 @@ #include "TolcInternal/run.hpp" #include "CommandLine/parse.hpp" +#include "Log/log.hpp" #include "TolcInternal/buildConfig.hpp" #include +#include #include #include -#include -#include -#include "Log/log.hpp" #include +#include #include -#include +#include namespace TolcInternal { @@ -24,6 +24,9 @@ callFrontend(TolcInternal::Config::Language language, case TolcInternal::Config::Language::Python: return Frontend::Python::createModule(globalNamespace, moduleName); break; + case TolcInternal::Config::Language::Wasm: + return Frontend::Wasm::createModule(globalNamespace, moduleName); + break; } return std::nullopt; } @@ -38,11 +41,15 @@ void writeFile(TolcInternal::Config const& config, std::string const& content) { std::filesystem::create_directories(config.outputDirectory); std::ofstream outFile; - outFile.open(config.outputDirectory / file); + auto outPath = config.outputDirectory / file; + outFile.open(outPath); if (outFile.is_open()) { - // Inject the input file aswell - outFile << "#include <" << config.inputFile.string() << ">\n" - << content; + if (outPath.extension().string() == ".cpp") { + // Inject the input file aswell + outFile << fmt::format("#include <{}>\n", + config.inputFile.string()); + } + outFile << content; } } diff --git a/tests/TolcInternal/buildConfig.cpp b/tests/TolcInternal/buildConfig.cpp index 6e2a331..6b30b8b 100644 --- a/tests/TolcInternal/buildConfig.cpp +++ b/tests/TolcInternal/buildConfig.cpp @@ -19,6 +19,16 @@ TEST_CASE("Translate valid cli to valid config", "[buildConfig]") { REQUIRE(config.has_value()); }; +TEST_CASE("Wasm gives Wasm :)", "[buildConfig]") { + auto cli = buildMockCLI(); + cli.language = "wasm"; + auto maybeConfig = TolcInternal::buildConfig(cli); + REQUIRE(maybeConfig.has_value()); + + auto config = maybeConfig.value(); + REQUIRE(config.language == TolcInternal::Config::Language::Wasm); +}; + TEST_CASE("Config has 'refined' the cli input", "[buildConfig]") { auto cli = buildMockCLI(); // Make sure it is python so we can check enum diff --git a/tests/TolcInternal/run.cpp b/tests/TolcInternal/run.cpp index e0f5138..409bc41 100644 --- a/tests/TolcInternal/run.cpp +++ b/tests/TolcInternal/run.cpp @@ -1,16 +1,21 @@ #include "TolcInternal/run.hpp" +#include "TolcInternal/buildConfig.hpp" #include #include #include #include +#include +#include #include #include TestUtil::CommandLineInput getValidCLI(std::filesystem::path const& inputFile, - std::vector includes = {}) { - std::string input = - "tolc python --output testOutDir --module-name myModule --input "; - input += inputFile.string(); + std::vector includes = {}, + std::string const& language = "python") { + std::string input = fmt::format( + "tolc {language} --output testOutDir --module-name myModule --input {input}", + fmt::arg("language", language), + fmt::arg("input", inputFile.string())); for (auto const& include : includes) { input += " -I " + include; @@ -18,13 +23,20 @@ TestUtil::CommandLineInput getValidCLI(std::filesystem::path const& inputFile, input += " --no-analytics"; + std::cout << input << '\n'; + return TestUtil::CommandLineInput(input); } TEST_CASE("Base cases", "[run]") { - auto cli = getValidCLI(TestUtil::getTestFilesDirectory() / "base.hpp"); - auto exitCode = TolcInternal::run(cli.argc, cli.argv); - REQUIRE(exitCode == 0); + for (std::string language : {"python", "wasm"}) { + CAPTURE(language); + auto cli = getValidCLI( + TestUtil::getTestFilesDirectory() / "base.hpp", {}, language); + std::cout << "Got a valid cli" << '\n'; + auto exitCode = TolcInternal::run(cli.argc, cli.argv); + REQUIRE(exitCode == 0); + } }; TEST_CASE("Standard library includes", "[run]") { diff --git a/tests/packaging/CMakeLists.txt b/tests/packaging/CMakeLists.txt index 71dc8ab..5601029 100644 --- a/tests/packaging/CMakeLists.txt +++ b/tests/packaging/CMakeLists.txt @@ -6,9 +6,26 @@ set(test_package ${CMAKE_CURRENT_BINARY_DIR}/test_package/tolc) add_test( NAME InstallPackage WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} - COMMAND ${CMAKE_COMMAND} --install ${CMAKE_BINARY_DIR} --prefix ${test_package}) + COMMAND ${CMAKE_COMMAND} --install ${CMAKE_BINARY_DIR} --prefix + ${test_package}) set_tests_properties(InstallPackage PROPERTIES FIXTURES_SETUP FixtureInstallPackage) +message( + STATUS "Using the local installation of tolc for tests: ${test_package}") + +include(${PROJECT_SOURCE_DIR}/cmake/GetEmscripten.cmake) + +# Will be used to compile wasm tests +if(NOT emsdk_SOURCE_DIR) + get_emscripten(VERSION 3.1.3) +endif() +# Export the toolchain so it can be used in system tests +set(emscripten_toolchain + ${emsdk_SOURCE_DIR}/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake +) + +message( + STATUS "Using the Emscripten toolchain for tests: ${emscripten_toolchain}") add_subdirectory(systemTests) add_subdirectory(moduleTests) diff --git a/tests/packaging/systemTests/CMakeLists.txt b/tests/packaging/systemTests/CMakeLists.txt index 73e41e4..7a5bde2 100644 --- a/tests/packaging/systemTests/CMakeLists.txt +++ b/tests/packaging/systemTests/CMakeLists.txt @@ -2,48 +2,85 @@ include_guard() # See the README for more detailed documentation -# Generate helpers for the tests -foreach(systemTestName translateFileTest translateLibraryWithHeaders translateLibraryTest addLibraryTest createTranslationTest) +function(function_name) + +endfunction() + +function(add_system_test) + # Define the supported set of keywords + set(prefix ARG) + set(noValues) + set(singleValues NAME LANGUAGE) + set(multiValues RUN_COMMAND) + # Process the arguments passed in + # can be used e.g. via ARG_TARGET + cmake_parse_arguments(${prefix} "${noValues}" "${singleValues}" + "${multiValues}" ${ARGN}) # Path to the consumer project - set(consumer ${CMAKE_CURRENT_LIST_DIR}/${systemTestName}) + set(consumer ${CMAKE_CURRENT_LIST_DIR}/${test_name}) + set(test_name ${ARG_NAME}) + set(language ${ARG_LANGUAGE}) + set(run_command ${ARG_RUN_COMMAND}) + + set(extra_cmake "") + if(language STREQUAL wasm) + set(extra_cmake -DCMAKE_TOOLCHAIN_FILE=${emscripten_toolchain}) + endif() # Generate the consumer build files # NOTE: Here we inject the path to the package that gets tested add_test( - NAME Generate${systemTestName} + NAME Generate${test_name} WORKING_DIRECTORY ${consumer} - COMMAND - ${CMAKE_COMMAND} -S. -Bbuild -Dtolc_ROOT=${test_package}) - set_tests_properties(Generate${systemTestName} PROPERTIES FIXTURES_SETUP - FixtureGenerate${systemTestName}) + COMMAND ${CMAKE_COMMAND} -S. -Bbuild -G Ninja + -Dtolc_DIR=${test_package}/lib/cmake/tolc ${extra_cmake}) + set_tests_properties(Generate${test_name} + PROPERTIES FIXTURES_SETUP FixtureGenerate${test_name}) # Need to install package before generating - set_tests_properties(Generate${systemTestName} PROPERTIES FIXTURES_REQUIRED - FixtureInstallPackage) + set_tests_properties(Generate${test_name} PROPERTIES FIXTURES_REQUIRED + FixtureInstallPackage) # Build the system test project add_test( - NAME Build${systemTestName} + NAME Build${test_name} WORKING_DIRECTORY ${consumer} COMMAND ${CMAKE_COMMAND} --build build) - set_tests_properties(Build${systemTestName} PROPERTIES FIXTURES_SETUP - FixtureBuild${systemTestName}) + set_tests_properties(Build${test_name} PROPERTIES FIXTURES_SETUP + FixtureBuild${test_name}) # Need to generate the build files before building - set_tests_properties(Build${systemTestName} PROPERTIES FIXTURES_REQUIRED - FixtureGenerate${systemTestName}) + set_tests_properties(Build${test_name} PROPERTIES FIXTURES_REQUIRED + FixtureGenerate${test_name}) # This requires that the project creates an executable called 'consumer' in the build directory - add_test(NAME Run${systemTestName} - COMMAND ${consumer}/build/consumer) - set_tests_properties(Run${systemTestName} PROPERTIES FIXTURES_SETUP - FixtureRun${systemTestName}) + add_test(NAME Run${test_name} COMMAND ${run_command}) + set_tests_properties(Run${test_name} PROPERTIES FIXTURES_SETUP + FixtureRun${test_name}) # Need to build the consumer before running it - set_tests_properties(Run${systemTestName} PROPERTIES FIXTURES_REQUIRED - FixtureBuild${systemTestName}) + set_tests_properties(Run${test_name} PROPERTIES FIXTURES_REQUIRED + FixtureBuild${test_name}) - add_test(NAME CleanUp${systemTestName} - COMMAND ${CMAKE_COMMAND} -E rm -r -f - ${consumer}/build) + add_test(NAME CleanUp${test_name} COMMAND ${CMAKE_COMMAND} -E rm -r -f + ${consumer}/build) # After running the final test, cleanup the build directory - set_tests_properties(CleanUp${systemTestName} PROPERTIES FIXTURES_REQUIRED - FixtureRun${systemTestName}) + set_tests_properties(CleanUp${test_name} PROPERTIES FIXTURES_REQUIRED + FixtureRun${test_name}) +endfunction() + +# Generate helpers for the tests +foreach(test_name translateFileTest translateLibraryWithHeaders + translateLibraryTest createTranslationTest) + add_system_test(NAME ${test_name} LANGUAGE python RUN_COMMAND + ${CMAKE_CURRENT_LIST_DIR}/${test_name}/build/consumer) +endforeach() + +foreach(test_name createTranslationTestWasm) + add_system_test( + NAME + ${test_name} + LANGUAGE + wasm + RUN_COMMAND + node + ${CMAKE_CURRENT_LIST_DIR}/${test_name}/test.js) endforeach() + diff --git a/tests/packaging/systemTests/addLibraryTest/CMakeLists.txt b/tests/packaging/systemTests/addLibraryTest/CMakeLists.txt deleted file mode 100644 index 6d9183a..0000000 --- a/tests/packaging/systemTests/addLibraryTest/CMakeLists.txt +++ /dev/null @@ -1,24 +0,0 @@ -# Need 3.11 for FetchContent module -cmake_minimum_required(VERSION 3.11) - -project(Consumer) - -# NOTE: Assumes tolc package is put into following path before executing this test -if(NOT tolc_ROOT) - message(FATAL_ERROR "Project ${PROJECT_NAME} did not recieve the tolc_ROOT variable. When configuring this project, inject it with -Dtolc_ROOT=...") -endif() -find_package(tolc CONFIG PATHS ${tolc_ROOT}) - -if(NOT tolc_FOUND) - message(FATAL_ERROR "No tolc package found! To test a specific version, place a prebuilt tolc package in ${tolc_ROOT}. This is normally done automatically.") -endif() - -add_executable(consumer src/consumer.cpp) - -tolc_add_library( - TARGET - myPythonModule - LANGUAGE - python - INPUT - src/myPythonModule.cpp) diff --git a/tests/packaging/systemTests/addLibraryTest/src/myPythonModule.cpp b/tests/packaging/systemTests/addLibraryTest/src/myPythonModule.cpp deleted file mode 100644 index cde05b8..0000000 --- a/tests/packaging/systemTests/addLibraryTest/src/myPythonModule.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include - -namespace py = pybind11; - -int add(int i, int j) { - return i + j; -} - -PYBIND11_MODULE(myPythonModule, myPythonModule) { - // NOTE: ^-- This name needs to be the same as the CMake target - - // Adding a simple function with optional help text - // and named variables (allows python to use add(i = 5, j = 3)) - myPythonModule.def("add", - &add, - "A function which adds two numbers", - py::arg("i"), - py::arg("j")); -} diff --git a/tests/packaging/systemTests/createTranslationTest/CMakeLists.txt b/tests/packaging/systemTests/createTranslationTest/CMakeLists.txt index 8582b70..a318755 100644 --- a/tests/packaging/systemTests/createTranslationTest/CMakeLists.txt +++ b/tests/packaging/systemTests/createTranslationTest/CMakeLists.txt @@ -4,27 +4,20 @@ cmake_minimum_required(VERSION 3.15) project(Consumer) # NOTE: Assumes tolc package is put into following path before executing this test -if(NOT tolc_ROOT) +if(NOT tolc_DIR) message( FATAL_ERROR - "Project ${PROJECT_NAME} did not recieve the tolc_ROOT variable. When configuring this project, inject it with -Dtolc_ROOT=..." - ) -endif() -find_package(tolc CONFIG PATHS ${tolc_ROOT}) - -if(NOT tolc_FOUND) - message( - FATAL_ERROR - "No tolc package found! To test a specific version, place a prebuilt tolc package in ${tolc_ROOT}. This is normally done automatically." + "Project ${PROJECT_NAME} did not recieve the tolc_DIR variable. When configuring this project, inject it with -Dtolc_DIR=..." ) endif() +find_package(tolc CONFIG PATHS ${tolc_DIR} REQUIRED) add_executable(consumer src/consumer.cpp) add_library(myLib src/myLib.cpp) target_include_directories(myLib PUBLIC include) -tolc_create_translation( +tolc_create_bindings( TARGET myLib LANGUAGE diff --git a/tests/packaging/systemTests/addLibraryTest/.gitignore b/tests/packaging/systemTests/createTranslationTestWasm/.gitignore similarity index 94% rename from tests/packaging/systemTests/addLibraryTest/.gitignore rename to tests/packaging/systemTests/createTranslationTestWasm/.gitignore index 8d66969..74291e4 100644 --- a/tests/packaging/systemTests/addLibraryTest/.gitignore +++ b/tests/packaging/systemTests/createTranslationTestWasm/.gitignore @@ -9,6 +9,7 @@ # Files !.gitignore !CMakeLists.txt +!test.js # Directories !src diff --git a/tests/packaging/systemTests/createTranslationTestWasm/CMakeLists.txt b/tests/packaging/systemTests/createTranslationTestWasm/CMakeLists.txt new file mode 100644 index 0000000..95d2311 --- /dev/null +++ b/tests/packaging/systemTests/createTranslationTestWasm/CMakeLists.txt @@ -0,0 +1,27 @@ +# Need 3.11 for FetchContent module +cmake_minimum_required(VERSION 3.15) + +project(Consumer) + +# NOTE: Assumes tolc package is put into following path before executing this test +if(NOT tolc_DIR) + message( + FATAL_ERROR + "Project ${PROJECT_NAME} did not recieve the tolc_DIR variable. When configuring this project, inject it with -Dtolc_DIR=..." + ) +endif() +find_package(tolc CONFIG PATHS ${tolc_DIR} REQUIRED) + +add_executable(consumer src/consumer.cpp) + +add_library(myLib src/myLib.cpp) +target_include_directories(myLib PUBLIC include) + +tolc_create_bindings( + TARGET + myLib + LANGUAGE + wasm + OUTPUT + ${CMAKE_CURRENT_BINARY_DIR}/out + NO_ANALYTICS) diff --git a/tests/packaging/systemTests/createTranslationTestWasm/include/myLib.hpp b/tests/packaging/systemTests/createTranslationTestWasm/include/myLib.hpp new file mode 100644 index 0000000..5516d62 --- /dev/null +++ b/tests/packaging/systemTests/createTranslationTestWasm/include/myLib.hpp @@ -0,0 +1,3 @@ +namespace myLib { +int getNumber(); +} // namespace myLibNS diff --git a/tests/packaging/systemTests/addLibraryTest/src/consumer.cpp b/tests/packaging/systemTests/createTranslationTestWasm/src/consumer.cpp similarity index 100% rename from tests/packaging/systemTests/addLibraryTest/src/consumer.cpp rename to tests/packaging/systemTests/createTranslationTestWasm/src/consumer.cpp diff --git a/tests/packaging/systemTests/createTranslationTestWasm/src/myLib.cpp b/tests/packaging/systemTests/createTranslationTestWasm/src/myLib.cpp new file mode 100644 index 0000000..5813248 --- /dev/null +++ b/tests/packaging/systemTests/createTranslationTestWasm/src/myLib.cpp @@ -0,0 +1,5 @@ +namespace myLib { +int getNumber() { + return 3; +} +} // namespace myLibNS diff --git a/tests/packaging/systemTests/createTranslationTestWasm/test.js b/tests/packaging/systemTests/createTranslationTestWasm/test.js new file mode 100644 index 0000000..4d99f6c --- /dev/null +++ b/tests/packaging/systemTests/createTranslationTestWasm/test.js @@ -0,0 +1,6 @@ +const loadmyLib = require('./build/tolc/myLib'); + +loadmyLib().then(myLib => { + console.log("Expecting a number:"); + console.log(myLib.myLib.getNumber()); +}); diff --git a/tests/packaging/systemTests/translateFileTest/CMakeLists.txt b/tests/packaging/systemTests/translateFileTest/CMakeLists.txt index 2b4e889..7c7e10b 100644 --- a/tests/packaging/systemTests/translateFileTest/CMakeLists.txt +++ b/tests/packaging/systemTests/translateFileTest/CMakeLists.txt @@ -4,20 +4,13 @@ cmake_minimum_required(VERSION 3.11) project(Consumer) # NOTE: Assumes tolc package is put into following path before executing this test -if(NOT tolc_ROOT) +if(NOT tolc_DIR) message( FATAL_ERROR - "Project ${PROJECT_NAME} did not recieve the tolc_ROOT variable. When configuring this project, inject it with -Dtolc_ROOT=..." - ) -endif() -find_package(tolc CONFIG PATHS ${tolc_ROOT}) - -if(NOT tolc_FOUND) - message( - FATAL_ERROR - "No tolc package found! To test a specific version, place a prebuilt tolc package in ${tolc_ROOT}. This is normally done automatically." + "Project ${PROJECT_NAME} did not recieve the tolc_DIR variable. When configuring this project, inject it with -Dtolc_DIR=..." ) endif() +find_package(tolc CONFIG PATHS ${tolc_DIR} REQUIRED) tolc_translate_file( MODULE_NAME diff --git a/tests/packaging/systemTests/translateLibraryTest/CMakeLists.txt b/tests/packaging/systemTests/translateLibraryTest/CMakeLists.txt index 453739e..a709bcc 100644 --- a/tests/packaging/systemTests/translateLibraryTest/CMakeLists.txt +++ b/tests/packaging/systemTests/translateLibraryTest/CMakeLists.txt @@ -4,20 +4,13 @@ cmake_minimum_required(VERSION 3.11) project(Consumer) # NOTE: Assumes tolc package is put into following path before executing this test -if(NOT tolc_ROOT) +if(NOT tolc_DIR) message( FATAL_ERROR - "Project ${PROJECT_NAME} did not recieve the tolc_ROOT variable. When configuring this project, inject it with -Dtolc_ROOT=..." - ) -endif() -find_package(tolc CONFIG PATHS ${tolc_ROOT}) - -if(NOT tolc_FOUND) - message( - FATAL_ERROR - "No tolc package found! To test a specific version, place a prebuilt tolc package in ${tolc_ROOT}. This is normally done automatically." + "Project ${PROJECT_NAME} did not recieve the tolc_DIR variable. When configuring this project, inject it with -Dtolc_DIR=..." ) endif() +find_package(tolc CONFIG PATHS ${tolc_DIR} REQUIRED) add_executable(consumer src/consumer.cpp) set_target_properties( diff --git a/tests/packaging/systemTests/translateLibraryWithHeaders/CMakeLists.txt b/tests/packaging/systemTests/translateLibraryWithHeaders/CMakeLists.txt index d5e40c6..2d0fb1b 100644 --- a/tests/packaging/systemTests/translateLibraryWithHeaders/CMakeLists.txt +++ b/tests/packaging/systemTests/translateLibraryWithHeaders/CMakeLists.txt @@ -4,20 +4,13 @@ cmake_minimum_required(VERSION 3.11) project(Consumer) # NOTE: Assumes tolc package is put into following path before executing this test -if(NOT tolc_ROOT) +if(NOT tolc_DIR) message( FATAL_ERROR - "Project ${PROJECT_NAME} did not recieve the tolc_ROOT variable. When configuring this project, inject it with -Dtolc_ROOT=..." - ) -endif() -find_package(tolc CONFIG PATHS ${tolc_ROOT}) - -if(NOT tolc_FOUND) - message( - FATAL_ERROR - "No tolc package found! To test a specific version, place a prebuilt tolc package in ${tolc_ROOT}. This is normally done automatically." + "Project ${PROJECT_NAME} did not recieve the tolc_DIR variable. When configuring this project, inject it with -Dtolc_DIR=..." ) endif() +find_package(tolc CONFIG PATHS ${tolc_DIR} REQUIRED) add_executable(consumer src/consumer.cpp) set_target_properties(