diff --git a/README.md b/README.md index abe2e3c6..e995dce3 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Djinni generator parses an interface definition file and generates: - Objective-C++ code to convert between C++ and Objective-C - Python and C code to convert between C++ and Python over CFFI - C++/CLI code to convert between C++ and C# +- C++ code to convert between WebAssembly and TS/JS ## Installation @@ -86,3 +87,5 @@ The code in this repository is in large portions copied from [dropbox/djinni](ht - Jacob Potter - Iulia Tamas - Andrew Twyman + +WebAssembly support is borrowed in large part from [snapchat/djinni](https://github.com/snapchat/djinni). \ No newline at end of file diff --git a/docs/cli-usage.md b/docs/cli-usage.md index b05d5bbf..f9e9840b 100644 --- a/docs/cli-usage.md +++ b/docs/cli-usage.md @@ -145,6 +145,18 @@ djinni \ | `--cppcli-namespace ...` | The namespace name to use for generated C++/CLI classes. | | `--cppcli-include-cpp-prefix <prefix>` | The prefix for `#include` of the main C++ header files from C++/CLI files. | +### WebAssembly/Typescript/Javascript + +| Argument | Description | +|----------------------------------------|----------------------------------------------------------------------------| +| `--wasm-out <out-folder>` | WebAssembly bridge code output folder. | +| `--wasm-include-prefix` | The prefix for #includes of WASM bridge C++ header files. | +| `--wasm-include-cpp-prefix` | The prefix for #includes of C++ header files. | +| `--wasm-base-lib-include-prefix` | The path prefix to be added to djinni support library inlcude lines in generated files | +| `--ts-out <out-folder>` | Path to the Typescript type definitions output folder | +| `--ts-module <module>` | Name of the module for the Typescript types. `module.ts` by default. | +| `--ts-support-files-out <out-folder>` | Path for where the support files `DjinniModule.[js\|ts]` should be generated. No support files are generated if the path is not specified. | + ### Yaml Generation @@ -230,6 +242,19 @@ Possible values: `FooBar`, `fooBar`, `foo_bar`, `FOO_BAR`, `m_fooBar`. | `--ident-cppcli-const` | `FooBar` | | `--ident-cppcli-file` | `FooBar` | +#### Javascript / Typescript + +| Argument | Default | +|-------------------------|-----------| +| `--ident-js-type` | `FooBar` | +| `--ident-js-type-param` | `FooBar` | +| `--ident-js-method` | `fooBar` | +| `--ident-js-local` | `fooBar` | +| `--ident-js-enum` | `FOO_BAR` | +| `--ident-js-const` | `FOO_BAR` | +| `--ident-js-field` | `fooBar` | + + Example: The djinni idl for an enum diff --git a/docs/generated-code-usage.md b/docs/generated-code-usage.md index c2f88304..ec72ea75 100644 --- a/docs/generated-code-usage.md +++ b/docs/generated-code-usage.md @@ -166,6 +166,42 @@ Add all generated files to your build target, and link against the [djinni-suppo C++/CLI sources have to be compiled with MSVC and the [`/clr` (Common Language Runtime Compilation)](https://docs.microsoft.com/en-us/cpp/build/reference/clr-common-language-runtime-compilation?view=msvc-160) option. +## TS/JS C++/WASM Project + +Djinni can generate code that bridges C++ (that compiles to Web Assembly) and Javascript/TypeScript in web browsers. + +For WASM, Djinni generates: +- C++ code, which should be compiled into the WASM bindary +- TypeScript code, provides optional TypeScript interface definitions + +The generated code can be used with both plain Javascript and TypeScript. + +Almost all Djinni features are supported, including importing external types via +yaml. + +Notable differences when comparing to the Java/ObjC support: + +- deriving(ord, eq) is not applicable to Javascript because JS doesn't support + overloading standard comparison methods. + +### Includes & Build target + +The following code will be generated for each defined type: + +| Type | C++ header | C++ source | WASM header/sources | TS definitions | +|------------|--------------------------|----------------------------|-------------------------------------|---------------------------| +| Enum/Flags | my\_enum.hpp | | my_enum.hpp, my_enum.cpp | module.ts :three: | +| | my\_enum+json.hpp :two: | | | DjinniModule.ts/js :four: | +| Record | my\_record.hpp | my\_record.cpp | my_record.hpp, my_enum.cpp | | +| | my\_record+json.hpp :two:| | | | +| Interface | my\_interface.hpp | my\_interface.cpp :one: | my_interface.hpp, my_interface.cpp | | + +- :one: Generated only for types that contain constants. +- :two: Generated only if cpp json serialization is enabled. +- :three: Name of file configurable via command-line options. +- :four: Generated if `ts-support-files-out` is specified at command line. + + ## C++ JSON Serialization support Serialization from C++ types to/from JSON is supported. This feature is currently only enabled for `nlohmann/json`, and if enabled creates `to_json`/`from_json` methods for all djinni records and enums. @@ -219,4 +255,4 @@ namespace nlohmann { } }; } -``` \ No newline at end of file +``` diff --git a/src/it/resources/cpp_interface_dependency.yml b/src/it/resources/cpp_interface_dependency.yml index bd697c50..32b32405 100644 --- a/src/it/resources/cpp_interface_dependency.yml +++ b/src/it/resources/cpp_interface_dependency.yml @@ -36,3 +36,10 @@ cs: header: '"InterfaceDependency.hpp"' typename: InterfaceDependency^ reference: true +wasm: + translator: '::InterfaceDependency' + header: '"InterfaceDependency.hpp"' + typename: InterfaceDependency +ts: + typename: InterfaceDependency + module: "InterfaceDependency.ts" diff --git a/src/it/resources/expected/all_datatypes/generated-files.txt b/src/it/resources/expected/all_datatypes/generated-files.txt index f5b6ec3a..ccea0aea 100644 --- a/src/it/resources/expected/all_datatypes/generated-files.txt +++ b/src/it/resources/expected/all_datatypes/generated-files.txt @@ -38,3 +38,11 @@ src/it/resources/result/all_datatypes/cwrapper/dh__map_int8_t_bool.cpp src/it/resources/result/all_datatypes/cwrapper-headers/dh__all_datatypes.h src/it/resources/result/all_datatypes/cwrapper/dh__all_datatypes.hpp src/it/resources/result/all_datatypes/cwrapper/dh__all_datatypes.cpp +src/it/resources/result/all_datatypes/wasm/enum_data.hpp +src/it/resources/result/all_datatypes/wasm/enum_data.cpp +src/it/resources/result/all_datatypes/wasm/all_datatypes.hpp +src/it/resources/result/all_datatypes/wasm/all_datatypes.cpp +src/it/resources/result/all_datatypes/ts/support-lib/DjinniModule.ts +src/it/resources/result/all_datatypes/ts/support-lib/DjinniModule.js +src/it/resources/result/all_datatypes/ts/module.ts + diff --git a/src/it/resources/expected/all_datatypes/ts/module.ts b/src/it/resources/expected/all_datatypes/ts/module.ts new file mode 100644 index 00000000..16f7e333 --- /dev/null +++ b/src/it/resources/expected/all_datatypes/ts/module.ts @@ -0,0 +1,33 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from all_datatypes.djinni + + +export enum EnumData { + A, + B, +} + +export interface /*record*/ AllDatatypes { + booleanData: boolean; + integer8Data: number; + integer16Data: number; + integer32Data: number; + integer64Data: bigint; + float32Data: number; + float64Data: number; + stringData: string; + binaryData: Uint8Array; + dateData: Date; + listData: Array<boolean>; + setData: Set<boolean>; + mapData: Map<number, boolean>; + optionalData?: boolean; + enumData: EnumData; +} + +export interface ns_testsuite { +} +export interface Module_statics { + + testsuite: ns_testsuite; +} diff --git a/src/it/resources/expected/all_datatypes/wasm/all_datatypes.cpp b/src/it/resources/expected/all_datatypes/wasm/all_datatypes.cpp new file mode 100644 index 00000000..ad5b46f2 --- /dev/null +++ b/src/it/resources/expected/all_datatypes/wasm/all_datatypes.cpp @@ -0,0 +1,46 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from all_datatypes.djinni + +#include "all_datatypes.hpp" // my header +#include "enum_data.hpp" + +namespace djinni_generated { + +auto AllDatatypes::toCpp(const JsType& j) -> CppType { + return {::djinni::Bool::Boxed::toCpp(j["booleanData"]), + ::djinni::I8::Boxed::toCpp(j["integer8Data"]), + ::djinni::I16::Boxed::toCpp(j["integer16Data"]), + ::djinni::I32::Boxed::toCpp(j["integer32Data"]), + ::djinni::I64::Boxed::toCpp(j["integer64Data"]), + ::djinni::F32::Boxed::toCpp(j["float32Data"]), + ::djinni::F64::Boxed::toCpp(j["float64Data"]), + ::djinni::String::Boxed::toCpp(j["stringData"]), + ::djinni::Binary::Boxed::toCpp(j["binaryData"]), + ::djinni::Date::Boxed::toCpp(j["dateData"]), + ::djinni::List<::djinni::Bool>::Boxed::toCpp(j["listData"]), + ::djinni::Set<::djinni::Bool>::Boxed::toCpp(j["setData"]), + ::djinni::Map<::djinni::I8, ::djinni::Bool>::Boxed::toCpp(j["mapData"]), + ::djinni::Optional<std::optional, ::djinni::Bool>::Boxed::toCpp(j["optionalData"]), + ::djinni_generated::EnumData::Boxed::toCpp(j["enumData"])}; +} +auto AllDatatypes::fromCpp(const CppType& c) -> JsType { + em::val js = em::val::object(); + js.set("booleanData", ::djinni::Bool::Boxed::fromCpp(c.booleanData)); + js.set("integer8Data", ::djinni::I8::Boxed::fromCpp(c.integer8Data)); + js.set("integer16Data", ::djinni::I16::Boxed::fromCpp(c.integer16Data)); + js.set("integer32Data", ::djinni::I32::Boxed::fromCpp(c.integer32Data)); + js.set("integer64Data", ::djinni::I64::Boxed::fromCpp(c.integer64Data)); + js.set("float32Data", ::djinni::F32::Boxed::fromCpp(c.float32Data)); + js.set("float64Data", ::djinni::F64::Boxed::fromCpp(c.float64Data)); + js.set("stringData", ::djinni::String::Boxed::fromCpp(c.stringData)); + js.set("binaryData", ::djinni::Binary::Boxed::fromCpp(c.binaryData)); + js.set("dateData", ::djinni::Date::Boxed::fromCpp(c.dateData)); + js.set("listData", ::djinni::List<::djinni::Bool>::Boxed::fromCpp(c.listData)); + js.set("setData", ::djinni::Set<::djinni::Bool>::Boxed::fromCpp(c.setData)); + js.set("mapData", ::djinni::Map<::djinni::I8, ::djinni::Bool>::Boxed::fromCpp(c.mapData)); + js.set("optionalData", ::djinni::Optional<std::optional, ::djinni::Bool>::Boxed::fromCpp(c.optionalData)); + js.set("enumData", ::djinni_generated::EnumData::Boxed::fromCpp(c.enum_data)); + return js; +} + +} // namespace djinni_generated diff --git a/src/it/resources/expected/all_datatypes/wasm/all_datatypes.hpp b/src/it/resources/expected/all_datatypes/wasm/all_datatypes.hpp new file mode 100644 index 00000000..e2f6a108 --- /dev/null +++ b/src/it/resources/expected/all_datatypes/wasm/all_datatypes.hpp @@ -0,0 +1,21 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from all_datatypes.djinni + +#pragma once + +#include "all_datatypes.hpp" +#include "djinni_wasm.hpp" + +namespace djinni_generated { + +struct AllDatatypes +{ + using CppType = ::testsuite::AllDatatypes; + using JsType = em::val; + using Boxed = AllDatatypes; + + static CppType toCpp(const JsType& j); + static JsType fromCpp(const CppType& c); +}; + +} // namespace djinni_generated diff --git a/src/it/resources/expected/all_datatypes/wasm/enum_data.cpp b/src/it/resources/expected/all_datatypes/wasm/enum_data.cpp new file mode 100644 index 00000000..95761b71 --- /dev/null +++ b/src/it/resources/expected/all_datatypes/wasm/enum_data.cpp @@ -0,0 +1,30 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from all_datatypes.djinni + +#include "enum_data.hpp" // my header +#include <mutex> + +namespace djinni_generated { + +namespace { + EM_JS(void, djinni_init_testsuite_enum_data_consts, (), { + Module.testsuite_EnumData = { + A : 0, + B : 1, + } + }) +} + +void EnumData::staticInitializeConstants() { + static std::once_flag initOnce; + std::call_once(initOnce, [] { + djinni_init_testsuite_enum_data_consts(); + ::djinni::djinni_register_name_in_ns("testsuite_EnumData", "testsuite.EnumData"); + }); +} + +EMSCRIPTEN_BINDINGS(testsuite_enum_data) { + EnumData::staticInitializeConstants(); +} + +} // namespace djinni_generated diff --git a/src/it/resources/expected/all_datatypes/wasm/enum_data.hpp b/src/it/resources/expected/all_datatypes/wasm/enum_data.hpp new file mode 100644 index 00000000..43d82fee --- /dev/null +++ b/src/it/resources/expected/all_datatypes/wasm/enum_data.hpp @@ -0,0 +1,15 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from all_datatypes.djinni + +#pragma once + +#include "djinni_wasm.hpp" +#include "enum_data.hpp" + +namespace djinni_generated { + +struct EnumData: ::djinni::WasmEnum<::testsuite::EnumData> { + static void staticInitializeConstants(); +}; + +} // namespace djinni_generated diff --git a/src/it/resources/expected/deprecation/generated-files.txt b/src/it/resources/expected/deprecation/generated-files.txt index f31025c9..8ff1673d 100644 --- a/src/it/resources/expected/deprecation/generated-files.txt +++ b/src/it/resources/expected/deprecation/generated-files.txt @@ -3,3 +3,7 @@ src/it/resources/result/deprecation/cpp-headers/my_flags.hpp src/it/resources/result/deprecation/cpp-headers/my_record.hpp src/it/resources/result/deprecation/cpp-headers/my_interface.hpp src/it/resources/result/deprecation/cpp/my_interface.cpp +src/it/resources/result/deprecation/ts/support-lib/DjinniModule.ts +src/it/resources/result/deprecation/ts/support-lib/DjinniModule.js +src/it/resources/result/deprecation/ts/module.ts + diff --git a/src/it/resources/expected/deprecation/ts/module.ts b/src/it/resources/expected/deprecation/ts/module.ts new file mode 100644 index 00000000..66be5e52 --- /dev/null +++ b/src/it/resources/expected/deprecation/ts/module.ts @@ -0,0 +1,78 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from deprecation.djinni + + +/** + * enum comment + * + * @deprecated Use something else + */ +export enum MyEnum { + /** @deprecated Use something else */ + OPTION1, + /** not deprecated */ + OPTION2, +} + +/** + * flags comment + * + * @deprecated Use someother flags + */ +export enum MyFlags { + /** @deprecated Use someother flag */ + FLAG1 = 1 << 0, + /** not deprecated */ + FLAG2 = 1 << 1, +} + +/** + * record comment + * + * @deprecated Use someother record + */ +export interface /*record*/ MyRecord { + /** @deprecated Use someother attribute */ + attribute: string; + /** not deprecated */ + another: string; + /** @deprecated Use someother attribute */ + again: string; +} +export namespace MyRecord { + /** @deprecated Use someother constant */ + export const VERSION = 1; +} + +/** + * interface comment + * + * @deprecated Use someother interface + */ +export interface MyInterface { + /** @deprecated Use someother method */ + methodA(value: number): void; + /** @deprecated Use someother method */ + methodB(value: number): void; + /** not deprecated */ + methodD(): void; + /** really im not */ + methodE(): void; +} +export namespace MyInterface { + /** @deprecated Use someother constant */ + export const VERSION = 1; +} +export interface MyInterface_statics { + /** @deprecated Use someother method */ + methodC(value: number): void; +} + +export interface ns_testsuite { + MyInterface: MyInterface_statics; +} +export interface Module_statics { + testsuite_MyInterface: MyInterface_statics; + + testsuite: ns_testsuite; +} diff --git a/src/it/resources/expected/my_client_interface/generated-files.txt b/src/it/resources/expected/my_client_interface/generated-files.txt index 09c24044..09e9aaad 100644 --- a/src/it/resources/expected/my_client_interface/generated-files.txt +++ b/src/it/resources/expected/my_client_interface/generated-files.txt @@ -14,3 +14,8 @@ src/it/resources/result/my_client_interface/cwrapper-headers/cw__my_client_inter src/it/resources/result/my_client_interface/cwrapper/cw__my_client_interface.hpp src/it/resources/result/my_client_interface/cwrapper/cw__my_client_interface.cpp src/it/resources/result/my_client_interface/cffi/pycffi_lib_build.py +src/it/resources/result/my_client_interface/wasm/my_client_interface.hpp +src/it/resources/result/my_client_interface/wasm/my_client_interface.cpp +src/it/resources/result/my_client_interface/ts/support-lib/DjinniModule.ts +src/it/resources/result/my_client_interface/ts/support-lib/DjinniModule.js +src/it/resources/result/my_client_interface/ts/module.ts diff --git a/src/it/resources/expected/my_client_interface/ts/module.ts b/src/it/resources/expected/my_client_interface/ts/module.ts new file mode 100644 index 00000000..f9258509 --- /dev/null +++ b/src/it/resources/expected/my_client_interface/ts/module.ts @@ -0,0 +1,10 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from my_client_interface.djinni + + +export interface MyClientInterface { + logString(str: string): boolean; +} + +export interface Module_statics { +} diff --git a/src/it/resources/expected/my_client_interface/wasm/my_client_interface.cpp b/src/it/resources/expected/my_client_interface/wasm/my_client_interface.cpp new file mode 100644 index 00000000..8863c847 --- /dev/null +++ b/src/it/resources/expected/my_client_interface/wasm/my_client_interface.cpp @@ -0,0 +1,16 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from my_client_interface.djinni + +#include "my_client_interface.hpp" // my header + +namespace djinni_generated { + + +EMSCRIPTEN_BINDINGS(_my_client_interface) { + em::class_<::MyClientInterface>("MyClientInterface") + .smart_ptr<std::shared_ptr<::MyClientInterface>>("MyClientInterface") + .function("nativeDestroy", &MyClientInterface::nativeDestroy) + ; +} + +} // namespace djinni_generated diff --git a/src/it/resources/expected/my_client_interface/wasm/my_client_interface.hpp b/src/it/resources/expected/my_client_interface/wasm/my_client_interface.hpp new file mode 100644 index 00000000..477cc61c --- /dev/null +++ b/src/it/resources/expected/my_client_interface/wasm/my_client_interface.hpp @@ -0,0 +1,27 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from my_client_interface.djinni + +#pragma once + +#include "djinni_wasm.hpp" +#include "my_client_interface.hpp" + +namespace djinni_generated { + +struct MyClientInterface : ::djinni::JsInterface<::MyClientInterface, MyClientInterface> { + using CppType = std::shared_ptr<::MyClientInterface>; + using CppOptType = std::shared_ptr<::MyClientInterface>; + using JsType = em::val; + using Boxed = MyClientInterface; + + static CppType toCpp(JsType j) { return _fromJs(j); } + static JsType fromCppOpt(const CppOptType& c) { return {_toJs(c)}; } + static JsType fromCpp(const CppType& c) { + ::djinni::checkForNull(c.get(), "MyClientInterface::fromCpp"); + return fromCppOpt(c); + } + + +}; + +} // namespace djinni_generated diff --git a/src/it/resources/expected/my_cpp_interface/generated-files.txt b/src/it/resources/expected/my_cpp_interface/generated-files.txt index 85da5b3a..082de032 100644 --- a/src/it/resources/expected/my_cpp_interface/generated-files.txt +++ b/src/it/resources/expected/my_cpp_interface/generated-files.txt @@ -16,3 +16,9 @@ src/it/resources/result/my_cpp_interface/cwrapper-headers/cw__my_cpp_interface.h src/it/resources/result/my_cpp_interface/cwrapper/cw__my_cpp_interface.hpp src/it/resources/result/my_cpp_interface/cwrapper/cw__my_cpp_interface.cpp src/it/resources/result/my_cpp_interface/cffi/pycffi_lib_build.py +src/it/resources/result/my_cpp_interface/wasm/my_cpp_interface.hpp +src/it/resources/result/my_cpp_interface/wasm/my_cpp_interface.cpp +src/it/resources/result/my_cpp_interface/ts/support-lib/DjinniModule.ts +src/it/resources/result/my_cpp_interface/ts/support-lib/DjinniModule.js +src/it/resources/result/my_cpp_interface/ts/module.ts + diff --git a/src/it/resources/expected/my_cpp_interface/ts/module.ts b/src/it/resources/expected/my_cpp_interface/ts/module.ts new file mode 100644 index 00000000..b6c2db7a --- /dev/null +++ b/src/it/resources/expected/my_cpp_interface/ts/module.ts @@ -0,0 +1,22 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from my_cpp_interface.djinni + + +/** interface comment */ +export interface MyCppInterface { + /** method comment */ + methodReturningNothing(value: number): void; + methodReturningSomeType(key: string): number; + methodChangingNothing(): number; +} +export namespace MyCppInterface { + /** Interfaces can also have constants */ + export const VERSION = 1; +} +export interface MyCppInterface_statics { + getVersion(): number; +} + +export interface Module_statics { + MyCppInterface: MyCppInterface_statics; +} diff --git a/src/it/resources/expected/my_cpp_interface/wasm/my_cpp_interface.cpp b/src/it/resources/expected/my_cpp_interface/wasm/my_cpp_interface.cpp new file mode 100644 index 00000000..4ab83b7c --- /dev/null +++ b/src/it/resources/expected/my_cpp_interface/wasm/my_cpp_interface.cpp @@ -0,0 +1,83 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from my_cpp_interface.djinni + +#include "my_cpp_interface.hpp" // my header + +namespace djinni_generated { + +em::val MyCppInterface::cppProxyMethods() { + static const em::val methods = em::val::array(std::vector<std::string> { + "methodReturningNothing", + "methodReturningSomeType", + "methodChangingNothing", + }); + return methods; +} + +void MyCppInterface::method_returning_nothing(const CppType& self, int32_t w_value) { + try { + self->method_returning_nothing(::djinni::I32::toCpp(w_value)); + } + catch(const std::exception& e) { + return ::djinni::ExceptionHandlingTraits<void>::handleNativeException(e); + } +} +int32_t MyCppInterface::method_returning_some_type(const CppType& self, const std::string& w_key) { + try { + auto r = self->method_returning_some_type(::djinni::String::toCpp(w_key)); + return ::djinni::I32::fromCpp(r); + } + catch(const std::exception& e) { + return ::djinni::ExceptionHandlingTraits<::djinni::I32>::handleNativeException(e); + } +} +int32_t MyCppInterface::method_changing_nothing(const CppType& self) { + try { + auto r = self->method_changing_nothing(); + return ::djinni::I32::fromCpp(r); + } + catch(const std::exception& e) { + return ::djinni::ExceptionHandlingTraits<::djinni::I32>::handleNativeException(e); + } +} +int32_t MyCppInterface::get_version() { + try { + auto r = ::MyCppInterface::get_version(); + return ::djinni::I32::fromCpp(r); + } + catch(const std::exception& e) { + return ::djinni::ExceptionHandlingTraits<::djinni::I32>::handleNativeException(e); + } +} + +EMSCRIPTEN_BINDINGS(_my_cpp_interface) { + em::class_<::MyCppInterface>("MyCppInterface") + .smart_ptr<std::shared_ptr<::MyCppInterface>>("MyCppInterface") + .function("nativeDestroy", &MyCppInterface::nativeDestroy) + .function("methodReturningNothing", MyCppInterface::method_returning_nothing) + .function("methodReturningSomeType", MyCppInterface::method_returning_some_type) + .function("methodChangingNothing", MyCppInterface::method_changing_nothing) + .class_function("getVersion", MyCppInterface::get_version) + ; +} + +namespace { + EM_JS(void, djinni_init__my_cpp_interface_consts, (), { + if (!('MyCppInterface' in Module)) { + Module.MyCppInterface = {}; + } + Module.MyCppInterface.VERSION = 1; + }) +} +void MyCppInterface::staticInitializeConstants() { + static std::once_flag initOnce; + std::call_once(initOnce, [] { + djinni_init__my_cpp_interface_consts(); + }); +} + +EMSCRIPTEN_BINDINGS(_my_cpp_interface_consts) { + MyCppInterface::staticInitializeConstants(); +} + +} // namespace djinni_generated diff --git a/src/it/resources/expected/my_cpp_interface/wasm/my_cpp_interface.hpp b/src/it/resources/expected/my_cpp_interface/wasm/my_cpp_interface.hpp new file mode 100644 index 00000000..698e7c2c --- /dev/null +++ b/src/it/resources/expected/my_cpp_interface/wasm/my_cpp_interface.hpp @@ -0,0 +1,34 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from my_cpp_interface.djinni + +#pragma once + +#include "djinni_wasm.hpp" +#include "my_cpp_interface.hpp" + +namespace djinni_generated { + +struct MyCppInterface : ::djinni::JsInterface<::MyCppInterface, MyCppInterface> { + using CppType = std::shared_ptr<::MyCppInterface>; + using CppOptType = std::shared_ptr<::MyCppInterface>; + using JsType = em::val; + using Boxed = MyCppInterface; + + static CppType toCpp(JsType j) { return _fromJs(j); } + static JsType fromCppOpt(const CppOptType& c) { return {_toJs(c)}; } + static JsType fromCpp(const CppType& c) { + ::djinni::checkForNull(c.get(), "MyCppInterface::fromCpp"); + return fromCppOpt(c); + } + + static em::val cppProxyMethods(); + + static void method_returning_nothing(const CppType& self, int32_t w_value); + static int32_t method_returning_some_type(const CppType& self, const std::string& w_key); + static int32_t method_changing_nothing(const CppType& self); + static int32_t get_version(); + + static void staticInitializeConstants(); +}; + +} // namespace djinni_generated diff --git a/src/it/resources/expected/my_enum/generated-files.txt b/src/it/resources/expected/my_enum/generated-files.txt index 4a177538..301dedf4 100644 --- a/src/it/resources/expected/my_enum/generated-files.txt +++ b/src/it/resources/expected/my_enum/generated-files.txt @@ -11,3 +11,9 @@ src/it/resources/result/my_enum/python/my_enum.py src/it/resources/result/my_enum/cwrapper-headers/dh__my_enum.h src/it/resources/result/my_enum/cwrapper/dh__my_enum.hpp src/it/resources/result/my_enum/cwrapper/dh__my_enum.cpp +src/it/resources/result/my_enum/wasm/my_enum.hpp +src/it/resources/result/my_enum/wasm/my_enum.cpp +src/it/resources/result/my_enum/ts/support-lib/DjinniModule.ts +src/it/resources/result/my_enum/ts/support-lib/DjinniModule.js +src/it/resources/result/my_enum/ts/module.ts + diff --git a/src/it/resources/expected/my_enum/ts/module.ts b/src/it/resources/expected/my_enum/ts/module.ts new file mode 100644 index 00000000..dc3710fb --- /dev/null +++ b/src/it/resources/expected/my_enum/ts/module.ts @@ -0,0 +1,18 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from my_enum.djinni + + +/** enum comment */ +export enum MyEnum { + /** enum option comment */ + OPTION1, + OPTION2, + OPTION3, +} + +export interface ns_testsuite { +} +export interface Module_statics { + + testsuite: ns_testsuite; +} diff --git a/src/it/resources/expected/my_enum/wasm/my_enum.cpp b/src/it/resources/expected/my_enum/wasm/my_enum.cpp new file mode 100644 index 00000000..34b79374 --- /dev/null +++ b/src/it/resources/expected/my_enum/wasm/my_enum.cpp @@ -0,0 +1,32 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from my_enum.djinni + +#include "my_enum.hpp" // my header +#include <mutex> + +namespace djinni_generated { + +namespace { + EM_JS(void, djinni_init_testsuite_my_enum_consts, (), { + Module.testsuite_MyEnum = { + /** enum option comment */ + OPTION1 : 0, + OPTION2 : 1, + OPTION3 : 2, + } + }) +} + +void MyEnum::staticInitializeConstants() { + static std::once_flag initOnce; + std::call_once(initOnce, [] { + djinni_init_testsuite_my_enum_consts(); + ::djinni::djinni_register_name_in_ns("testsuite_MyEnum", "testsuite.MyEnum"); + }); +} + +EMSCRIPTEN_BINDINGS(testsuite_my_enum) { + MyEnum::staticInitializeConstants(); +} + +} // namespace djinni_generated diff --git a/src/it/resources/expected/my_enum/wasm/my_enum.hpp b/src/it/resources/expected/my_enum/wasm/my_enum.hpp new file mode 100644 index 00000000..ac3221c0 --- /dev/null +++ b/src/it/resources/expected/my_enum/wasm/my_enum.hpp @@ -0,0 +1,15 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from my_enum.djinni + +#pragma once + +#include "djinni_wasm.hpp" +#include "my_enum.hpp" + +namespace djinni_generated { + +struct MyEnum: ::djinni::WasmEnum<::testsuite::MyEnum> { + static void staticInitializeConstants(); +}; + +} // namespace djinni_generated diff --git a/src/it/resources/expected/my_flags/generated-files.txt b/src/it/resources/expected/my_flags/generated-files.txt index 197936ff..991833a6 100644 --- a/src/it/resources/expected/my_flags/generated-files.txt +++ b/src/it/resources/expected/my_flags/generated-files.txt @@ -11,3 +11,9 @@ src/it/resources/result/my_flags/python/my_flags.py src/it/resources/result/my_flags/cwrapper-headers/dh__my_flags.h src/it/resources/result/my_flags/cwrapper/dh__my_flags.hpp src/it/resources/result/my_flags/cwrapper/dh__my_flags.cpp +src/it/resources/result/my_flags/wasm/my_flags.hpp +src/it/resources/result/my_flags/wasm/my_flags.cpp +src/it/resources/result/my_flags/ts/support-lib/DjinniModule.ts +src/it/resources/result/my_flags/ts/support-lib/DjinniModule.js +src/it/resources/result/my_flags/ts/module.ts + diff --git a/src/it/resources/expected/my_flags/ts/module.ts b/src/it/resources/expected/my_flags/ts/module.ts new file mode 100644 index 00000000..88b35769 --- /dev/null +++ b/src/it/resources/expected/my_flags/ts/module.ts @@ -0,0 +1,16 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from my_flags.djinni + + +/** flag comment */ +export enum MyFlags { + NO_FLAGS = 0, + /** flag option comment */ + FLAG1 = 1 << 0, + FLAG2 = 1 << 1, + FLAG3 = 1 << 2, + ALL_FLAGS = 0 | (1 << 0) | (1 << 1) | (1 << 2), +} + +export interface Module_statics { +} diff --git a/src/it/resources/expected/my_flags/wasm/my_flags.cpp b/src/it/resources/expected/my_flags/wasm/my_flags.cpp new file mode 100644 index 00000000..f7faf82e --- /dev/null +++ b/src/it/resources/expected/my_flags/wasm/my_flags.cpp @@ -0,0 +1,33 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from my_flags.djinni + +#include "my_flags.hpp" // my header +#include <mutex> + +namespace djinni_generated { + +namespace { + EM_JS(void, djinni_init__my_flags_consts, (), { + Module.MyFlags = { + NO_FLAGS : 0, + /** flag option comment */ + FLAG1 : 1 << 0, + FLAG2 : 1 << 1, + FLAG3 : 1 << 2, + ALL_FLAGS : 0 | (1 << 0) | (1 << 1) | (1 << 2), + } + }) +} + +void MyFlags::staticInitializeConstants() { + static std::once_flag initOnce; + std::call_once(initOnce, [] { + djinni_init__my_flags_consts(); + }); +} + +EMSCRIPTEN_BINDINGS(_my_flags) { + MyFlags::staticInitializeConstants(); +} + +} // namespace djinni_generated diff --git a/src/it/resources/expected/my_flags/wasm/my_flags.hpp b/src/it/resources/expected/my_flags/wasm/my_flags.hpp new file mode 100644 index 00000000..73ff8b78 --- /dev/null +++ b/src/it/resources/expected/my_flags/wasm/my_flags.hpp @@ -0,0 +1,15 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from my_flags.djinni + +#pragma once + +#include "djinni_wasm.hpp" +#include "my_flags.hpp" + +namespace djinni_generated { + +struct MyFlags: ::djinni::WasmEnum<::MyFlags> { + static void staticInitializeConstants(); +}; + +} // namespace djinni_generated diff --git a/src/it/resources/expected/my_record/generated-files.txt b/src/it/resources/expected/my_record/generated-files.txt index 026b9239..aae0971a 100644 --- a/src/it/resources/expected/my_record/generated-files.txt +++ b/src/it/resources/expected/my_record/generated-files.txt @@ -24,3 +24,9 @@ src/it/resources/result/my_record/cwrapper/dh__map_string_int32_t.cpp src/it/resources/result/my_record/cwrapper-headers/dh__my_record.h src/it/resources/result/my_record/cwrapper/dh__my_record.hpp src/it/resources/result/my_record/cwrapper/dh__my_record.cpp +src/it/resources/result/my_record/wasm/my_record.hpp +src/it/resources/result/my_record/wasm/my_record.cpp +src/it/resources/result/my_record/ts/support-lib/DjinniModule.ts +src/it/resources/result/my_record/ts/support-lib/DjinniModule.js +src/it/resources/result/my_record/ts/module.ts + diff --git a/src/it/resources/expected/my_record/ts/module.ts b/src/it/resources/expected/my_record/ts/module.ts new file mode 100644 index 00000000..82525bff --- /dev/null +++ b/src/it/resources/expected/my_record/ts/module.ts @@ -0,0 +1,22 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from my_record.djinni + + +/** record comment */ +export interface /*record*/ MyRecord { + /** record property comment */ + id: number; + info: string; + store: Set<string>; + hash: Map<string, number>; +} +export namespace MyRecord { + export const STRING_CONST = "Constants can be put here"; +} + +export interface ns_testsuite { +} +export interface Module_statics { + + testsuite: ns_testsuite; +} diff --git a/src/it/resources/expected/my_record/wasm/my_record.cpp b/src/it/resources/expected/my_record/wasm/my_record.cpp new file mode 100644 index 00000000..1d3d157d --- /dev/null +++ b/src/it/resources/expected/my_record/wasm/my_record.cpp @@ -0,0 +1,43 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from my_record.djinni + +#include "my_record.hpp" // my header + +namespace djinni_generated { + +auto MyRecord::toCpp(const JsType& j) -> CppType { + return {::djinni::I32::Boxed::toCpp(j["id"]), + ::djinni::String::Boxed::toCpp(j["info"]), + ::djinni::Set<::djinni::String>::Boxed::toCpp(j["store"]), + ::djinni::Map<::djinni::String, ::djinni::I32>::Boxed::toCpp(j["hash"])}; +} +auto MyRecord::fromCpp(const CppType& c) -> JsType { + em::val js = em::val::object(); + js.set("id", ::djinni::I32::Boxed::fromCpp(c.id)); + js.set("info", ::djinni::String::Boxed::fromCpp(c.info)); + js.set("store", ::djinni::Set<::djinni::String>::Boxed::fromCpp(c.store)); + js.set("hash", ::djinni::Map<::djinni::String, ::djinni::I32>::Boxed::fromCpp(c.hash)); + return js; +} + +namespace { + EM_JS(void, djinni_init_testsuite_my_record_consts, (), { + if (!('testsuite_MyRecord' in Module)) { + Module.testsuite_MyRecord = {}; + } + Module.testsuite_MyRecord.STRING_CONST = "Constants can be put here"; + }) +} +void MyRecord::staticInitializeConstants() { + static std::once_flag initOnce; + std::call_once(initOnce, [] { + djinni_init_testsuite_my_record_consts(); + ::djinni::djinni_register_name_in_ns("testsuite_MyRecord", "testsuite.MyRecord"); + }); +} + +EMSCRIPTEN_BINDINGS(testsuite_my_record_consts) { + MyRecord::staticInitializeConstants(); +} + +} // namespace djinni_generated diff --git a/src/it/resources/expected/my_record/wasm/my_record.hpp b/src/it/resources/expected/my_record/wasm/my_record.hpp new file mode 100644 index 00000000..d557bb72 --- /dev/null +++ b/src/it/resources/expected/my_record/wasm/my_record.hpp @@ -0,0 +1,22 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from my_record.djinni + +#pragma once + +#include "djinni_wasm.hpp" +#include "my_record.hpp" + +namespace djinni_generated { + +struct MyRecord +{ + using CppType = ::testsuite::MyRecord; + using JsType = em::val; + using Boxed = MyRecord; + + static CppType toCpp(const JsType& j); + static JsType fromCpp(const CppType& c); + static void staticInitializeConstants(); +}; + +} // namespace djinni_generated diff --git a/src/it/resources/expected/using_custom_datatypes/generated-files.txt b/src/it/resources/expected/using_custom_datatypes/generated-files.txt index afc8d921..29bfea27 100644 --- a/src/it/resources/expected/using_custom_datatypes/generated-files.txt +++ b/src/it/resources/expected/using_custom_datatypes/generated-files.txt @@ -30,3 +30,10 @@ src/it/resources/result/using_custom_datatypes/cwrapper/dh__custom_datatype.cpp src/it/resources/result/using_custom_datatypes/cwrapper-headers/dh__other_record.h src/it/resources/result/using_custom_datatypes/cwrapper/dh__other_record.hpp src/it/resources/result/using_custom_datatypes/cwrapper/dh__other_record.cpp +src/it/resources/result/using_custom_datatypes/wasm/custom_datatype.hpp +src/it/resources/result/using_custom_datatypes/wasm/custom_datatype.cpp +src/it/resources/result/using_custom_datatypes/wasm/other_record.hpp +src/it/resources/result/using_custom_datatypes/wasm/other_record.cpp +src/it/resources/result/using_custom_datatypes/ts/support-lib/DjinniModule.ts +src/it/resources/result/using_custom_datatypes/ts/support-lib/DjinniModule.js +src/it/resources/result/using_custom_datatypes/ts/module.ts diff --git a/src/it/resources/expected/using_custom_datatypes/ts/module.ts b/src/it/resources/expected/using_custom_datatypes/ts/module.ts new file mode 100644 index 00000000..e8a946f3 --- /dev/null +++ b/src/it/resources/expected/using_custom_datatypes/ts/module.ts @@ -0,0 +1,14 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from using_custom_datatypes.djinni + + +export interface /*record*/ CustomDatatype { + recordData: string; +} + +export interface /*record*/ OtherRecord { + customDatatypeData: CustomDatatype; +} + +export interface Module_statics { +} diff --git a/src/it/resources/expected/using_custom_datatypes/wasm/custom_datatype.cpp b/src/it/resources/expected/using_custom_datatypes/wasm/custom_datatype.cpp new file mode 100644 index 00000000..43bdc04f --- /dev/null +++ b/src/it/resources/expected/using_custom_datatypes/wasm/custom_datatype.cpp @@ -0,0 +1,17 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from using_custom_datatypes.djinni + +#include "custom_datatype.hpp" // my header + +namespace djinni_generated { + +auto CustomDatatype::toCpp(const JsType& j) -> CppType { + return {::djinni::String::Boxed::toCpp(j["recordData"])}; +} +auto CustomDatatype::fromCpp(const CppType& c) -> JsType { + em::val js = em::val::object(); + js.set("recordData", ::djinni::String::Boxed::fromCpp(c.recordData)); + return js; +} + +} // namespace djinni_generated diff --git a/src/it/resources/expected/using_custom_datatypes/wasm/custom_datatype.hpp b/src/it/resources/expected/using_custom_datatypes/wasm/custom_datatype.hpp new file mode 100644 index 00000000..87fdcd06 --- /dev/null +++ b/src/it/resources/expected/using_custom_datatypes/wasm/custom_datatype.hpp @@ -0,0 +1,21 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from using_custom_datatypes.djinni + +#pragma once + +#include "custom_datatype.hpp" +#include "djinni_wasm.hpp" + +namespace djinni_generated { + +struct CustomDatatype +{ + using CppType = ::CustomDatatype; + using JsType = em::val; + using Boxed = CustomDatatype; + + static CppType toCpp(const JsType& j); + static JsType fromCpp(const CppType& c); +}; + +} // namespace djinni_generated diff --git a/src/it/resources/expected/using_custom_datatypes/wasm/other_record.cpp b/src/it/resources/expected/using_custom_datatypes/wasm/other_record.cpp new file mode 100644 index 00000000..7f2941f6 --- /dev/null +++ b/src/it/resources/expected/using_custom_datatypes/wasm/other_record.cpp @@ -0,0 +1,18 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from using_custom_datatypes.djinni + +#include "other_record.hpp" // my header +#include "custom_datatype.hpp" + +namespace djinni_generated { + +auto OtherRecord::toCpp(const JsType& j) -> CppType { + return {::djinni_generated::CustomDatatype::Boxed::toCpp(j["customDatatypeData"])}; +} +auto OtherRecord::fromCpp(const CppType& c) -> JsType { + em::val js = em::val::object(); + js.set("customDatatypeData", ::djinni_generated::CustomDatatype::Boxed::fromCpp(c.customDatatypeData)); + return js; +} + +} // namespace djinni_generated diff --git a/src/it/resources/expected/using_custom_datatypes/wasm/other_record.hpp b/src/it/resources/expected/using_custom_datatypes/wasm/other_record.hpp new file mode 100644 index 00000000..7332226f --- /dev/null +++ b/src/it/resources/expected/using_custom_datatypes/wasm/other_record.hpp @@ -0,0 +1,21 @@ +// AUTOGENERATED FILE - DO NOT MODIFY! +// This file was generated by Djinni from using_custom_datatypes.djinni + +#pragma once + +#include "djinni_wasm.hpp" +#include "other_record.hpp" + +namespace djinni_generated { + +struct OtherRecord +{ + using CppType = ::OtherRecord; + using JsType = em::val; + using Boxed = OtherRecord; + + static CppType toCpp(const JsType& j); + static JsType fromCpp(const CppType& c); +}; + +} // namespace djinni_generated diff --git a/src/it/scala/djinni/GeneratorIntegrationTest.scala b/src/it/scala/djinni/GeneratorIntegrationTest.scala index 9e12584c..c88b6ff2 100644 --- a/src/it/scala/djinni/GeneratorIntegrationTest.scala +++ b/src/it/scala/djinni/GeneratorIntegrationTest.scala @@ -29,7 +29,9 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen { "pyCffiFilenames", "cWrapperFilenames", "cWrapperHeaderFilenames", - "cppcliFilenames" + "cppcliFilenames", + "wasmFilenames", + "tsFileNames" ), ( "my_enum", @@ -46,7 +48,9 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen { PyCffi(), CWrapper("dh__my_enum.cpp", "dh__my_enum.hpp"), CWrapperHeaders("dh__my_enum.h"), - CppCli("MyEnum.hpp", "MyEnum.cpp") + CppCli("MyEnum.hpp", "MyEnum.cpp"), + Wasm("my_enum.hpp", "my_enum.cpp"), + Ts("module.ts") ), ( "my_flags", @@ -63,7 +67,9 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen { PyCffi(), CWrapper("dh__my_flags.cpp", "dh__my_flags.hpp"), CWrapperHeaders("dh__my_flags.h"), - CppCli("MyFlags.hpp", "MyFlags.cpp") + CppCli("MyFlags.hpp", "MyFlags.cpp"), + Wasm("my_flags.hpp", "my_flags.cpp"), + Ts("module.ts") ), ( "my_record", @@ -96,7 +102,9 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen { "dh__my_record.h", "dh__set_string.h" ), - CppCli("MyRecord.hpp", "MyRecord.cpp") + CppCli("MyRecord.hpp", "MyRecord.cpp"), + Wasm("my_record.hpp", "my_record.cpp"), + Ts("module.ts") ), ( "my_cpp_interface", @@ -113,7 +121,9 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen { PyCffi("pycffi_lib_build.py"), CWrapper("cw__my_cpp_interface.cpp", "cw__my_cpp_interface.hpp"), CWrapperHeaders("cw__my_cpp_interface.h"), - CppCli("MyCppInterface.hpp", "MyCppInterface.cpp") + CppCli("MyCppInterface.hpp", "MyCppInterface.cpp"), + Wasm("my_cpp_interface.hpp", "my_cpp_interface.cpp"), + Ts("module.ts") ), ( "my_client_interface", @@ -130,7 +140,9 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen { PyCffi("pycffi_lib_build.py"), CWrapper("cw__my_client_interface.cpp", "cw__my_client_interface.hpp"), CWrapperHeaders("cw__my_client_interface.h"), - CppCli("MyClientInterface.hpp", "MyClientInterface.cpp") + CppCli("MyClientInterface.hpp", "MyClientInterface.cpp"), + Wasm("my_client_interface.hpp", "my_client_interface.cpp"), + Ts("module.ts") ), ( "all_datatypes", @@ -176,7 +188,14 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen { "AllDatatypes.cpp", "EnumData.cpp", "EnumData.hpp" - ) + ), + Wasm( + "all_datatypes.hpp", + "all_datatypes.cpp", + "enum_data.hpp", + "enum_data.cpp" + ), + Ts("module.ts") ), ( "using_custom_datatypes", @@ -207,7 +226,9 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen { "dh__other_record.hpp" ), CWrapperHeaders("dh__custom_datatype.h", "dh__other_record.h"), - CppCli("CustomDatatype.hpp", "CustomDatatype.cpp") + CppCli("CustomDatatype.hpp", "CustomDatatype.cpp"), + Wasm("custom_datatype.hpp", "custom_datatype.cpp"), + Ts("module.ts") ) ) forAll(djinniTypes) { @@ -226,7 +247,9 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen { pyCffiFilenames: PyCffi, cWrapperFilenames: CWrapper, cWrapperHeaderFilenames: CWrapperHeaders, - cppcliFilenames: CppCli + cppcliFilenames: CppCli, + wasmFilenames: Wasm, + tsFileNames: Ts ) => it(s"should generate valid language bridges for `$idlFile`-types") { Given(s"`$idlFile.djinni`") @@ -337,6 +360,7 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen { python = false, cWrapper = false, cppCLI = false, + wasm = false, useNNHeader = true ) djinni(cmd) @@ -370,6 +394,7 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen { python = false, cWrapper = false, cppCLI = true, + wasm = false, useNNHeader = false ) djinni(cmd) @@ -407,6 +432,7 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen { python = false, cWrapper = false, cppCLI = true, + wasm = false, useNNHeader = false ) djinni(cmd) @@ -439,7 +465,8 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen { python = false, cWrapper = false, cppCLI = false, - cppOmitDefaultRecordCtor = true + cppOmitDefaultRecordCtor = true, + wasm = false ) djinni(cmd) @@ -474,6 +501,7 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen { python = false, cWrapper = false, cppCLI = true, + wasm = false, useNNHeader = true ) djinni(cmd) @@ -556,6 +584,7 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen { python = false, cWrapper = false, cppCLI = false, + wasm = false, cppOmitDefaultRecordCtor = true ) djinni(cmd) @@ -587,6 +616,7 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen { python = false, cWrapper = false, cppCLI = true, + wasm = false, cppOmitDefaultRecordCtor = true ) @@ -618,6 +648,7 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen { python = false, cWrapper = false, cppCLI = false, + wasm = false, cppOmitDefaultRecordCtor = true ) @@ -787,6 +818,7 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen { python = false, cWrapper = false, cppCLI = false, + wasm = false, cppJsonSerialization = Some("nlohmann_json") ) djinni(cmd) @@ -861,6 +893,7 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen { python = false, cWrapper = false, cppCLI = false, + wasm = false, cppOmitDefaultRecordCtor = true ) @@ -891,6 +924,7 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen { python = false, cWrapper = false, cppCLI = false, + wasm = false, cppOmitDefaultRecordCtor = true ) @@ -925,6 +959,7 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen { python = false, cWrapper = false, cppCLI = false, + wasm = false, cppOmitDefaultRecordCtor = true ) @@ -935,4 +970,35 @@ class GeneratorIntegrationTest extends IntegrationTest with GivenWhenThen { ) assertFileContentEquals(idlFile, OBJC_HEADERS, objcHeaderFilenames) } + + it( + "should generate @Deprecated annotations for TS from @deprecated notes in comments" + ) { + Given( + "an IDL-file that documents deprecation using @deprecated in comments" + ) + val idlFile = "deprecation" + + When("generating Wasm/TS source") + val tsFilenames = + Ts("module.ts") + val cmd = djinniParams( + idlFile, + cpp = false, + objc = false, + java = false, + python = false, + cWrapper = false, + cppCLI = false, + wasm = true, + cppOmitDefaultRecordCtor = true + ) + + djinni(cmd) + + Then( + "the @deprecated comments are generated as @Deprecated annotations" + ) + assertFileContentEquals(idlFile, TS, tsFilenames) + } } diff --git a/src/it/scala/djinni/IntegrationTest.scala b/src/it/scala/djinni/IntegrationTest.scala index 8db8d13a..685a8d74 100644 --- a/src/it/scala/djinni/IntegrationTest.scala +++ b/src/it/scala/djinni/IntegrationTest.scala @@ -28,6 +28,8 @@ class IntegrationTest extends AnyFunSpec { final val CWRAPPER = "cwrapper" final val CWRAPPER_HEADERS = "cwrapper-headers" final val CPPCLI = "cppcli" + final val WASM = "wasm" + final val TS = "ts" type Cpp = List[String] def Cpp(params: String*) = List(params: _*) @@ -57,6 +59,10 @@ class IntegrationTest extends AnyFunSpec { def CWrapperHeaders(params: String*) = List(params: _*) type CppCli = List[String] def CppCli(params: String*) = List(params: _*) + type Wasm = List[String] + def Wasm(params: String*) = List(params: _*) + type Ts = List[String] + def Ts(params: String*) = List(params: _*) /** Executes the djinni generator with the given parameters * @param parameters @@ -108,6 +114,7 @@ class IntegrationTest extends AnyFunSpec { python: Boolean = true, cWrapper: Boolean = true, cppCLI: Boolean = true, + wasm: Boolean = true, useNNHeader: Boolean = false, cppOmitDefaultRecordCtor: Boolean = false, cppJsonSerialization: Option[String] = None @@ -145,6 +152,12 @@ class IntegrationTest extends AnyFunSpec { cmd += s" --cppcli-out $baseOutputPath/$idl/$CPPCLI" cmd += s" --cppcli-include-cpp-prefix ../$CPP_HEADERS/" } + if (wasm) { + cmd += s" --wasm-out $baseOutputPath/$idl/$WASM" + cmd += s" --wasm-namespace testsuite" + cmd += s" --ts-out $baseOutputPath/$idl/$TS" + cmd += s" --ts-support-files-out $baseOutputPath/$idl/$TS/support-lib" + } if (useNNHeader) { cmd += " --cpp-nn-header nn.hpp" cmd += " --cpp-nn-type dropbox::oxygen::nn_shared_ptr" diff --git a/src/main/resources/ts/DjinniModule.js b/src/main/resources/ts/DjinniModule.js new file mode 100644 index 00000000..0580a9f6 --- /dev/null +++ b/src/main/resources/ts/DjinniModule.js @@ -0,0 +1,17 @@ +"use strict"; +/** + * Copyright 2021 Snap, Inc. + * + * 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. + */ +exports.__esModule = true; diff --git a/src/main/resources/ts/DjinniModule.ts b/src/main/resources/ts/DjinniModule.ts new file mode 100644 index 00000000..d8b0f1d0 --- /dev/null +++ b/src/main/resources/ts/DjinniModule.ts @@ -0,0 +1,19 @@ +/** + * Copyright 2021 Snap, Inc. + * + * 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. + */ + +export interface DjinniModule { + allocateWasmBuffer(size: number): Uint8Array; +} diff --git a/src/main/scala/djinni/CppMarshal.scala b/src/main/scala/djinni/CppMarshal.scala index 33c377b9..c21c48a7 100644 --- a/src/main/scala/djinni/CppMarshal.scala +++ b/src/main/scala/djinni/CppMarshal.scala @@ -271,15 +271,17 @@ class CppMarshal(spec: Spec) extends Marshal(spec) { } } case None => - if (isOptionalInterface(tm)) { - // otherwise, interfaces are always plain old shared_ptr - expr(tm.args.head) - } else { - val args = - if (tm.args.isEmpty) "" - else tm.args.map(expr).mkString("<", ", ", ">") - base(tm.base) + args + val ty = if (isOptionalInterface(tm)) { tm.args.head } + else { tm } + val prefix = if (!isInterface(ty)) { "" } + else { /* isInterface */ + if (isOptional(tm)) { "/*nullable*/ " } + else { "/*not-null*/ " } } + val args = + if (ty.args.isEmpty) "" + else ty.args.map(expr).mkString("<", ", ", ">") + prefix + base(ty.base) + args } } expr(tm) diff --git a/src/main/scala/djinni/JavaMarshal.scala b/src/main/scala/djinni/JavaMarshal.scala index 2c56b7e8..3d3b47c3 100644 --- a/src/main/scala/djinni/JavaMarshal.scala +++ b/src/main/scala/djinni/JavaMarshal.scala @@ -103,9 +103,9 @@ class JavaMarshal(spec: Spec) extends Marshal(spec) { } def isEnumFlags(m: Meta): Boolean = m match { - case MDef(_, _, _, Enum(_, true)) => true - case MExtern(_, _, _, Enum(_, true), _, _, _, _, _, _) => true - case _ => false + case MDef(_, _, _, Enum(_, true)) => true + case MExtern(_, _, _, Enum(_, true), _, _, _, _, _, _, _, _) => true + case _ => false } def isEnumFlags(tm: MExpr): Boolean = tm.base match { case MOptional => isEnumFlags(tm.args.head) diff --git a/src/main/scala/djinni/Main.scala b/src/main/scala/djinni/Main.scala index a667588c..e3e3b6c3 100644 --- a/src/main/scala/djinni/Main.scala +++ b/src/main/scala/djinni/Main.scala @@ -108,6 +108,17 @@ object Main { var pycffiOutFolder: Option[File] = None var pyImportPrefix: String = "" var cppJsonSerialization: Option[String] = None + var wasmOutFolder: Option[File] = None + var wasmIncludePrefix: String = "" + var wasmIncludeCppPrefix: String = "" + var wasmBaseLibIncludePrefix: String = "" + var wasmOmitConstants: Boolean = false + var wasmNamespace: Option[String] = None + var wasmOmitNsAlias: Boolean = false + var jsIdentStyle = IdentStyle.jsDefault + var tsOutFolder: Option[File] = None + var tsModule: String = "module" + var tsSupportFilesOutFolder: Option[File] = None val argParser: OptionParser[Unit] = new scopt.OptionParser[Unit]("djinni") { @@ -543,6 +554,64 @@ object Main { .text( "Way of specifying if file generation should be skipped (default: false)" ) + note("wasm\n") + opt[File]("wasm-out") + .valueName("<out-folder>") + .foreach(x => wasmOutFolder = Some(x)) + .text( + "The output for the WASM bridge C++ files (Generator disabled if unspecified)." + ) + opt[String]("wasm-include-prefix") + .valueName("<prefix>") + .foreach(wasmIncludePrefix = _) + .text( + "The prefix for #includes of WASM header files from WASM C++ files." + ) + opt[String]("wasm-include-cpp-prefix") + .valueName("<prefix>") + .foreach(wasmIncludeCppPrefix = _) + .text( + "The prefix for #includes of the main header files from WASM C++ files." + ) + opt[String]("wasm-base-lib-include-prefix") + .valueName("...") + .foreach(x => wasmBaseLibIncludePrefix = x) + .text( + "The WASM base library's include path, relative to the WASM C++ classes." + ) + opt[Boolean]("wasm-omit-constants") + .valueName("<true/false>") + .foreach(x => wasmOmitConstants = x) + .text( + "Omit the generation of consts and enums in wasm, making them only accessible through TypeScript." + ) + opt[String]("wasm-namespace") + .valueName("...") + .foreach(x => wasmNamespace = Some(x)) + .text("The namespace to use for generated Wasm classes.") + opt[Boolean]("wasm-omit-namespace-alias") + .valueName("<true/false>") + .foreach(x => wasmOmitNsAlias = x) + .text( + "Omit the generation of namespace aliases for classes. Namespaces will be prepended to class names instead." + ) + opt[File]("ts-out") + .valueName("<out-folder>") + .foreach(x => tsOutFolder = Some(x)) + .text( + "The output for the TypeScript interface files (Generator disabled if unspecified)." + ) + opt[String]("ts-module") + .valueName("<name>") + .foreach(tsModule = _) + .text("TypeScript declaration module name (default: \"module\").") + + opt[File]("ts-support-files-out") + .valueName("<out-folder>") + .foreach(x => tsSupportFilesOutFolder = Some(x)) + .text( + "Folder in which to generate DjinniModule.[ts/js] files. (Not generated if not specified)" + ) note( "\n\nIdentifier styles (ex: \"FooBar\", \"fooBar\", \"foo_bar\", \"FOO_BAR\", \"m_fooBar\")" @@ -733,6 +802,43 @@ object Main { "FooBar", c => { cppCliIdentStyle = cppCliIdentStyle.copy(file = c) } ) + + note("\nTypescript/Javascript options:") + identStyle( + "ident-js-type", + "FooBar", + c => { jsIdentStyle = jsIdentStyle.copy(ty = c) } + ) + identStyle( + "ident-js-type-param", + "FooBar", + c => { jsIdentStyle = jsIdentStyle.copy(typeParam = c) } + ) + identStyle( + "ident-js-method", + "fooBar", + c => { jsIdentStyle = jsIdentStyle.copy(method = c) } + ) + identStyle( + "ident-js-local", + "fooBar", + c => { jsIdentStyle = jsIdentStyle.copy(local = c) } + ) + identStyle( + "ident-js-enum", + "FOO_BAR", + c => { jsIdentStyle = jsIdentStyle.copy(enum = c) } + ) + identStyle( + "ident-js-field", + "fooBar", + c => { jsIdentStyle = jsIdentStyle.copy(field = c) } + ) + identStyle( + "ident-js-const", + "FOO_BAR", + c => { jsIdentStyle = jsIdentStyle.copy(const = c) } + ) } if (argParser.parse(args, ()).isEmpty) { @@ -829,7 +935,9 @@ object Main { objcppOutRequired = objcppOutFolder.isDefined, javaOutRequired = javaOutFolder.isDefined, jniOutRequired = jniOutFolder.isDefined, - cppCliOutRequired = cppCliOutFolder.isDefined + cppCliOutRequired = cppCliOutFolder.isDefined, + wasmOutRequired = wasmOutFolder.isDefined, + tsOutRequired = wasmOutFolder.isDefined ) match { case Some(err) => System.err.println(err) @@ -938,7 +1046,19 @@ object Main { cWrapperIncludePrefix, cWrapperIncludeCppPrefix, pyImportPrefix, - cppJsonSerialization + cppJsonSerialization, + wasmOutFolder, + wasmIncludePrefix, + wasmIncludeCppPrefix, + wasmBaseLibIncludePrefix, + wasmOmitConstants, + wasmNamespace, + wasmOmitNsAlias, + jsIdentStyle, + tsOutFolder, + tsModule, + tsSupportFilesOutFolder, + idlFile.getName.stripSuffix(".djinni") ) try { diff --git a/src/main/scala/djinni/TsGenerator.scala b/src/main/scala/djinni/TsGenerator.scala new file mode 100644 index 00000000..11a2f8a7 --- /dev/null +++ b/src/main/scala/djinni/TsGenerator.scala @@ -0,0 +1,393 @@ +/** Copyright 2021 Snap, Inc. + * + * 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. + */ + +package djinni + +import java.io._ +import djinni.ast.Record.DerivingType +import djinni.ast._ +import djinni.generatorTools._ +import djinni.meta._ +import djinni.writer.IndentWriter +import scala.collection.mutable.ListBuffer +import scala.collection.mutable.TreeSet +import java.util.regex.Pattern +import java.util.regex.Matcher + +import scala.io.Source + +class TsGenerator(spec: Spec) extends Generator(spec) { + private def tsRetType(m: Interface.Method): String = { + return if (m.ret.isEmpty) "void" else toTsType(m.ret.get.resolved) + } + + private def tsPrimitiveType(p: MPrimitive): String = p._idlName match { + case "bool" => "boolean" + case "i64" => "bigint" + case _ => "number" + } + + // return the base type if tm is optional otherwise None + private def optionalBase(tm: MExpr): Option[MExpr] = { + tm.base match { + case MOptional => Some(tm.args.head) + case _ => None + } + } + + private def removeOptional(tm: MExpr): MExpr = { + tm.base match { + case MOptional => tm.args.head + case _ => tm + } + } + + private def nullityAnnotation(tm: MExpr) = tm.base match { + case MOptional => " | undefined" + case _ => "" + } + + def toTsType(tm: MExpr, addNullability: Boolean = true): String = { + def args(tm: MExpr) = if (tm.args.isEmpty) "" + else + tm.args.map(arg => toTsType(arg, addNullability)).mkString("<", ", ", ">") + def f(tm: MExpr): String = { + tm.base match { + case MOptional => + assert(tm.args.size == 1) + val arg = tm.args.head + arg.base match { + case MOptional => throw new AssertionError("nested optional?") + case m => f(arg) + } + case e: MExtern => e.ts.typename + (if (e.ts.generic) args(tm) else "") + case o => + val base = o match { + case p: MPrimitive => tsPrimitiveType(p) + case MString => "string" + case MDate => "Date" + case MBinary => "Uint8Array" + case MOptional => + throw new AssertionError( + "optional should have been special cased" + ) + case MList => "Array" + case MSet => "Set" + case MMap => "Map" + case d: MDef => idJs.ty(d.name) + case e: MExtern => throw new AssertionError("unreachable") + case p: MParam => idJs.typeParam(p.name) + } + base + args(tm) + } + } + f(tm) + (if (addNullability) nullityAnnotation(tm) else "") + } + + case class TsSymbolRef(sym: String, module: String) + def references(m: Meta): Seq[TsSymbolRef] = m match { + case e: MExtern => List(TsSymbolRef(idJs.ty(e.name), e.ts.module)) + case _ => List() + } + class TsRefs() { + var imports = scala.collection.mutable.Map[String, TreeSet[String]]() + + def find(ty: TypeRef) { find(ty.resolved) } + def find(tm: MExpr) { + tm.args.foreach(find) + find(tm.base) + } + def find(m: Meta) = for (r <- references(m)) r match { + case TsSymbolRef(sym, module) => { + var syms = imports.getOrElseUpdate(module, TreeSet[String]()) + syms += (sym) + } + case _ => + } + } + + private def generateTsConstants( + w: IndentWriter, + ident: Ident, + consts: Seq[Const] + ) = { + def writeJsConst(w: IndentWriter, ty: TypeRef, v: Any): Unit = v match { + case l: Long if (toTsType(removeOptional(ty.resolved)) == "bigint") => + w.w(s"""BigInt("${l.toString}")""") + case l: Long => w.w(l.toString) + case d: Double => w.w(d.toString) + case b: Boolean => w.w(if (b) "true" else "false") + case s: String => w.w(s) + case e: EnumValue => w.w(s"${idJs.ty(ty.expr.ident)}.${idJs.enum(e)}") + case v: ConstRef => w.w(s"${idJs.const(v)}") + case z: Any => { // Value is record + val recordMdef = ty.resolved.base.asInstanceOf[MDef] + val record = recordMdef.body.asInstanceOf[Record] + val vMap = z.asInstanceOf[Map[String, Any]] + w.w("").braced { + // Use exact sequence + val skipFirst = SkipFirst() + for (f <- record.fields) { + skipFirst { w.wl(",") } + w.w(s"${idJs.field(f.ident)}: ") + writeJsConst(w, f.ty, vMap.apply(f.ident.name)) + } + w.wl + } + } + } + w.w(s"export namespace ${idJs.ty(ident)}").braced { + for (c <- consts) { + writeDoc(w, c.doc) + w.w(s"export const ${idJs.const(c.ident)} = ") + writeJsConst(w, c.ty, c.value) + w.wl(";") + } + } + } + + // ------------------------------------------------------------------------ + private def generateEnum( + origin: String, + ident: Ident, + doc: Doc, + e: Enum, + w: IndentWriter + ) { + w.wl + writeDoc(w, doc) + w.w(s"export enum ${idJs.ty(ident)}").braced { + writeEnumOptionNone(w, e, idJs.enum) + writeEnumOptions( + w, + e, + idJs.enum, + (o: Enum.Option, shift: Int) => s" = 1 << $shift," + ) + writeEnumOptionAll(w, e, idJs.enum) + } + } + private def generateRecord( + origin: String, + ident: Ident, + doc: Doc, + params: Seq[TypeParam], + r: Record, + w: IndentWriter + ) { + w.wl + writeDoc(w, doc) + w.w(s"export interface /*record*/ ${idJs.ty(ident)}").braced { + for (f <- r.fields) { + writeDoc(w, f.doc) + optionalBase(f.ty.resolved) match { + case Some(t) => w.wl(s"${idJs.field(f.ident)}?: ${toTsType(t)};") + case _ => w.wl(s"${idJs.field(f.ident)}: ${toTsType(f.ty.resolved)};") + } + } + } + if (!r.consts.isEmpty) { + generateTsConstants(w, ident, r.consts); + } + } + private def generateInterface( + origin: String, + ident: Ident, + doc: Doc, + typeParams: Seq[TypeParam], + i: Interface, + w: IndentWriter + ) { + w.wl + writeDoc(w, doc) + w.w(s"export interface ${idJs.ty(ident)}").braced { + for (m <- i.methods.filter(!_.static)) { + writeMethodDoc(w, m, idJs.local) + w.w(s"${idJs.method(m.ident)}(") + w.w( + m.params + .map(p => s"${idJs.local(p.ident)}: ${toTsType(p.ty.resolved)}") + .mkString(", ") + ) + w.wl(s"): ${tsRetType(m)};") + } + } + if (!i.consts.isEmpty) { + generateTsConstants(w, ident, i.consts); + } + val staticMethods = i.methods.filter(m => m.static && m.lang.js) + if (!staticMethods.isEmpty) { + w.w(s"export interface ${idJs.ty(ident)}_statics").braced { + for (m <- staticMethods) { + writeMethodDoc(w, m, idJs.local) + w.w(s"${idJs.method(m.ident)}(") + w.w( + m.params + .map(p => s"${idJs.method(p.ident)}: ${toTsType(p.ty.resolved)}") + .mkString(", ") + ) + w.wl(s"): ${tsRetType(m)};") + } + } + } + } + private def withWasmNamespace(name: String, sep: String = "_") = + spec.wasmNamespace match { + case Some(p) => + p.replaceAll( + Pattern.quote("."), + Matcher.quoteReplacement(sep) + ) + sep + name + case None => name + } + // -------------------------------------------------------------------------- + override def generate(idl: Seq[TypeDecl]) { + if (!spec.tsSupportFilesOutFolder.isEmpty) { + writeDjinniModuleFilesFile() + } + createFile( + spec.tsOutFolder.get, + spec.tsModule + ".ts", + (w: IndentWriter) => { + w.wl("// AUTOGENERATED FILE - DO NOT MODIFY!") + w.wl( + "// This file was generated by Djinni from " + spec.moduleName + ".djinni" + ) + w.wl + val decls = idl.collect { case itd: InternTypeDecl => itd } + + // find external references + val refs = new TsRefs() + for (td <- decls) td.body match { + case r: Record => { + r.fields.foreach(f => refs.find(f.ty)) + r.consts.foreach(c => refs.find(c.ty)) + } + case i: Interface => { + i.methods.foreach(m => { + m.params.foreach(p => refs.find(p.ty)) + m.ret.foreach(refs.find) + }) + i.consts.foreach(c => refs.find(c.ty)) + } + case _ => + } + // write external references + for ((module, syms) <- refs.imports) { + if (module != "") { + w.wl(s"""import { ${syms.mkString(", ")} } from "$module"""") + } + } + + var interfacesWithStatics = new ListBuffer[String]() + for (td <- decls) td.body match { + case e: Enum => generateEnum(td.origin, td.ident, td.doc, e, w) + case r: Record => + generateRecord(td.origin, td.ident, td.doc, td.params, r, w) + case i: Interface => { + generateInterface(td.origin, td.ident, td.doc, td.params, i, w) + if (i.methods.exists(m => m.static && m.lang.js)) { + interfacesWithStatics += idJs.ty(td.ident.name) + } + } + case _ => + } + // add static factories + w.wl + if (!spec.wasmOmitNsAlias && !spec.wasmNamespace.isEmpty) { + val nsParts = spec.wasmNamespace.get.split("\\.") + + for (i <- 0 until nsParts.length - 1) { + w.w(s"export interface ns_${nsParts(i)}").braced { + w.wl(s"${nsParts(i + 1)}: ns_${nsParts(i + 1)}") + } + } + w.w(s"export interface ns_${nsParts.last}").braced { + for (i <- interfacesWithStatics.toList) { + w.wl(i + ": " + i + "_statics;") + } + } + w.w(s"export interface ${idJs.ty(spec.tsModule)}_statics").braced { + for (i <- interfacesWithStatics.toList) { + w.wl(withWasmNamespace(i) + ": " + i + "_statics;") + } + w.wl + w.wl(s"${nsParts.head}: ns_${nsParts.head};") + } + } else { + w.w(s"export interface ${idJs.ty(spec.tsModule)}_statics").braced { + for (i <- interfacesWithStatics.toList) { + w.wl(withWasmNamespace(i) + ": " + i + "_statics;") + } + } + } + } + ) + } + override def generateEnum(origin: String, ident: Ident, doc: Doc, e: Enum) {} + override def generateRecord( + origin: String, + ident: Ident, + doc: Doc, + params: Seq[TypeParam], + r: Record + ) {} + override def generateInterface( + origin: String, + ident: Ident, + doc: Doc, + typeParams: Seq[TypeParam], + i: Interface + ) {} + + def writeDjinniModuleFile(f: IndentWriter => Unit, ext: String): Unit = { + if (!spec.skipGeneration) { + createFolder("TS Support lib", spec.tsSupportFilesOutFolder.get) + createFileOnce( + spec.tsSupportFilesOutFolder.get, + s"DjinniModule.$ext", + (w: IndentWriter) => { + w.wl("// AUTOGENERATED FILE - DO NOT MODIFY!") + w.wl("// This file was generated by Djinni") + w.wl + f(w) + } + ) + } + } + + def writeDjinniModuleFilesFile(): Unit = { + val tsContent = Source + .fromResource("ts/DjinniModule.ts") + .getLines + .mkString("\n") + writeDjinniModuleFile( + w => { + w.wl(tsContent) + }, + "ts" + ) + val jsContent = Source + .fromResource("ts/DjinniModule.js") + .getLines + .mkString("\n") + writeDjinniModuleFile( + w => { + w.wl(jsContent) + }, + "js" + ) + } +} diff --git a/src/main/scala/djinni/WasmGenerator.scala b/src/main/scala/djinni/WasmGenerator.scala new file mode 100644 index 00000000..0e5591f9 --- /dev/null +++ b/src/main/scala/djinni/WasmGenerator.scala @@ -0,0 +1,697 @@ +/** Copyright 2021 Snap, Inc. + * + * 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. + */ + +package djinni + +import djinni.ast.Record.DerivingType +import djinni.ast._ +import djinni.generatorTools._ +import djinni.meta._ +import djinni.writer.IndentWriter + +import scala.collection.mutable +import java.util.regex.Pattern +import java.util.regex.Matcher + +class WasmGenerator(spec: Spec) extends Generator(spec) { + + val cppMarshal = new CppMarshal(spec) + + private def wasmFilenameStyle(name: String): String = { + return spec.jniFileIdentStyle(name) + } + private def helperNamespace(): String = { + return spec.jniNamespace; + } + + private def helperClass(name: String) = spec.jniClassIdentStyle(name) + private def helperClass(tm: MExpr): String = + helperName(tm) + helperTemplates(tm) + def helperName(tm: MExpr): String = tm.base match { + case d: MDef => withNs(Some(helperNamespace()), helperClass(d.name)) + case e: MExtern => e.wasm.translator + case o => + withNs( + Some("djinni"), + o match { + case p: MPrimitive => + p.idlName match { + case "i8" => "I8" + case "i16" => "I16" + case "i32" => "I32" + case "i64" => "I64" + case "f32" => "F32" + case "f64" => "F64" + case "bool" => "Bool" + } + case MOptional => "Optional" + case MBinary => "Binary" + case MString => if (spec.cppUseWideStrings) "WString" else "String" + case MDate => "Date" + case MList => "List" + case MSet => "Set" + case MMap => "Map" + case d: MDef => throw new AssertionError("unreachable") + case e: MExtern => throw new AssertionError("unreachable") + case p: MParam => throw new AssertionError("not applicable") + } + ) + } + private def helperTemplates(tm: MExpr): String = { + def f() = if (tm.args.isEmpty) "" + else tm.args.map(helperClass).mkString("<", ", ", ">") + tm.base match { + case MOptional => + assert(tm.args.size == 1) + val argHelperClass = helperClass(tm.args.head) + s"<${spec.cppOptionalTemplate}, $argHelperClass>" + case MList | MSet => + assert(tm.args.size == 1) + f + case MMap => + assert(tm.args.size == 2) + f + case _ => f + } + } + + def wasmType(tm: MExpr): String = tm.base match { + case p: MPrimitive => p.cName + case MString => + if (spec.cppUseWideStrings) "std::wstring" else "std::string" + case d: MDef => + d.defType match { + case DEnum => "int32_t" + case _ => "em::val" + } + case e: MExtern => e.wasm.typename + case _ => "em::val" + } + def wasmType(t: TypeRef): String = wasmType(t.resolved) + + private def stubRetType(m: Interface.Method): String = { + return if (m.ret.isEmpty) "void" else wasmType(m.ret.get) + } + private def stubParamType(t: TypeRef): String = t.resolved.base match { + case p: MPrimitive => p.cName + case MString => + if (spec.cppUseWideStrings) "const std::wstring&" + else "const std::string&" + case d: MDef => + d.defType match { + case DEnum => "int32_t" + case _ => "const em::val&" + } + case e: MExtern => + e.defType match { + case DEnum => e.wasm.typename + case _ => "const " + e.wasm.typename + "&" + } + case _ => "const em::val&" + } + + private def stubParamName(name: String): String = s"w_${idCpp.local(name)}" + + def jsClassNameAsCppType(jsClass: String): String = { + val classNameChars = jsClass.toList.map(c => s"'$c'") + s"""::djinni::JsClassName<${classNameChars.mkString(",")}>""" + } + + def include(ident: String) = q( + spec.wasmIncludePrefix + wasmFilenameStyle(ident) + "." + spec.cppHeaderExt + ) + + def references(m: Meta, exclude: String = ""): Seq[SymbolReference] = + m match { + case d: MDef => List(ImportRef(include(d.name))) + case e: MExtern => List(ImportRef(resolveExtWasmHdr(e.wasm.header))) + case _ => List() + } + + def resolveExtWasmHdr(path: String) = { + path.replaceAll("\\$", spec.wasmBaseLibIncludePrefix); + } + + class WasmRefs(name: String, cppPrefixOverride: Option[String] = None) { + var hpp = mutable.TreeSet[String]() + var cpp = mutable.TreeSet[String]() + + val cppPrefix = cppPrefixOverride.getOrElse(spec.wasmIncludeCppPrefix) + hpp.add( + "#include " + q( + cppPrefix + spec.cppFileIdentStyle(name) + "." + spec.cppHeaderExt + ) + ) + hpp.add("#include " + q(spec.wasmBaseLibIncludePrefix + "djinni_wasm.hpp")) + spec.cppNnHeader match { + case Some(nnHdr) => hpp.add("#include " + nnHdr) + case _ => + } + + def find(ty: TypeRef) { find(ty.resolved) } + def find(tm: MExpr) { + tm.args.foreach(find) + find(tm.base) + } + def find(m: Meta) = for (r <- references(m, name)) r match { + case ImportRef(arg) => cpp.add("#include " + arg) + case _ => + } + } + + private def generateWasmConstants( + w: IndentWriter, + ident: Ident, + consts: Seq[Const] + ) { + val helper = helperClass(ident) + var dependentTypes = mutable.TreeSet[String]() + def writeJsConst(w: IndentWriter, ty: TypeRef, v: Any): Unit = v match { + case l: Long if wasmType(ty).equalsIgnoreCase("int64_t") => + w.w(s"""BigInt("${l.toString}")""") + case l: Long => w.w(l.toString) + case d: Double => w.w(d.toString) + case b: Boolean => w.w(if (b) "true" else "false") + case s: String => w.w(s) + case e: EnumValue => { + w.w( + s"Module.${withWasmNamespace(idJs.ty(ty.expr.ident))}.${idJs.enum(e)}" + ) + dependentTypes.add(helperClass(ty.expr.ident)) + } + case v: ConstRef => { + w.w(s"Module.${withWasmNamespace(idJs.ty(ident))}.${idJs.const(v)}") + } + case z: Map[_, _] => { // Value is record + val recordMdef = ty.resolved.base.asInstanceOf[MDef] + val record = recordMdef.body.asInstanceOf[Record] + val vMap = z.asInstanceOf[Map[String, Any]] + w.w("").braced { + // Use exact sequence + val skipFirst = SkipFirst() + for (f <- record.fields) { + skipFirst { w.wl(",") } + w.w(s"${idJs.field(f.ident)}: ") + writeJsConst(w, f.ty, vMap.apply(f.ident.name)) + } + w.wl + } + } + } + w.wl + val fullyQualifiedName = withWasmNamespace(idJs.ty(ident)) + w.w(s"namespace").braced { + w.wl( + s"EM_JS(void, djinni_init_${withCppNamespace(ident.name)}_consts, (), {" + ).nested { + w.w(s"if (!('${fullyQualifiedName}' in Module))").braced { + w.wl(s"Module.${fullyQualifiedName} = {};") + } + for (c <- consts) { + w.w(s"Module.${fullyQualifiedName}.${idJs.const(c.ident)} = ") + writeJsConst(w, c.ty, c.value) + w.wl(";") + } + } + w.wl("})") + } + w.w(s"void $helper::staticInitializeConstants()").braced { + w.wl("static std::once_flag initOnce;") + w.wl(s"std::call_once(initOnce, [] {") + w.wl(s" djinni_init_${withCppNamespace(ident.name)}_consts();") + if (!spec.wasmOmitNsAlias && !spec.wasmNamespace.isEmpty) { + w.wl( + s""" ::djinni::djinni_register_name_in_ns("${fullyQualifiedName}", "${spec.wasmNamespace.get}.${idJs + .ty(ident)}");""" + ) + } + w.wl(s"});") + } + w.wl + w.w(s"EMSCRIPTEN_BINDINGS(${withCppNamespace(ident.name)}_consts)").braced { + for (d <- dependentTypes) { + if (d != helper) + w.wl(s"$d::staticInitializeConstants();"); + } + w.wl(s"$helper::staticInitializeConstants();"); + } + } + + // ------------------------------------------------------------------------------ + + override def generateEnum(origin: String, ident: Ident, doc: Doc, e: Enum) { + val refs = new WasmRefs(ident.name) + refs.cpp.add("#include <mutex>") + val cls = cppMarshal.fqTypename(ident, e) + val helper = helperClass(ident) + val fullyQualifiedName = withWasmNamespace(idJs.ty(ident)) + writeHppFileGeneric( + spec.wasmOutFolder.get, + helperNamespace(), + wasmFilenameStyle + )( + ident.name, + origin, + refs.hpp, + Nil, + (w => { + w.w(s"struct $helper: ::djinni::WasmEnum<$cls>").bracedSemi { + if (!spec.wasmOmitConstants) { + w.wl("static void staticInitializeConstants();"); + } + } + }), + (w => {}) + ) + if (!spec.wasmOmitConstants) { + writeCppFileGeneric( + spec.wasmOutFolder.get, + helperNamespace(), + wasmFilenameStyle, + spec.wasmIncludePrefix + )( + ident.name, + origin, + refs.cpp, + (w => { + w.w(s"namespace").braced { + w.wl( + s"EM_JS(void, djinni_init_${withCppNamespace(ident.name)}_consts, (), {" + ).nested { + w.w(s"Module.${fullyQualifiedName} = ").braced { + writeEnumOptionNone(w, e, idJs.enum, () => ": 0,") + writeEnumOptions( + w, + e, + idJs.enum, + (o: Enum.Option, shift: Int) => s": 1 << $shift,", + (ordinal: Int) => s": $ordinal," + ) + writeEnumOptionAll( + w, + e, + idJs.enum, + (ordinalsAndNames: Seq[Tuple2[Int, String]]) => + s""": ${ordinalsAndNames + .map(e => e._1) + .fold("0")((acc, o) => acc + s" | (1 << $o)")},""" + ) + } + } + w.wl("})") + } + w.wl + w.w(s"void $helper::staticInitializeConstants()").braced { + w.wl("static std::once_flag initOnce;") + w.wl(s"std::call_once(initOnce, [] {") + w.wl(s" djinni_init_${withCppNamespace(ident.name)}_consts();") + if (!spec.wasmOmitNsAlias && !spec.wasmNamespace.isEmpty) { + w.wl( + s""" ::djinni::djinni_register_name_in_ns("${fullyQualifiedName}", "${spec.wasmNamespace.get}.${idJs + .ty(ident)}");""" + ) + } + w.wl(s"});") + } + w.wl + w.w(s"EMSCRIPTEN_BINDINGS(${withCppNamespace(ident.name)})").braced { + w.wl(s"$helper::staticInitializeConstants();") + } + }) + ) + } + } + + override def generateInterface( + origin: String, + ident: Ident, + doc: Doc, + typeParams: Seq[TypeParam], + i: Interface + ) { + val refs = new WasmRefs(ident.name) + i.consts.foreach(c => refs.find(c.ty)) + i.methods.foreach(m => { + m.params.foreach(p => refs.find(p.ty)) + m.ret.foreach(refs.find) + }) + + val cls = withNs(Some(spec.cppNamespace), idCpp.ty(ident)) + val helper = helperClass(ident) + + writeHppFileGeneric( + spec.wasmOutFolder.get, + helperNamespace(), + wasmFilenameStyle + )( + ident.name, + origin, + refs.hpp, + Nil, + (w => { + w.w(s"struct $helper : ::djinni::JsInterface<$cls, $helper>").bracedSemi { + // types + w.wl(s"using CppType = std::shared_ptr<$cls>;") + w.wl(s"using CppOptType = std::shared_ptr<$cls>;") + w.wl("using JsType = em::val;") + w.wl(s"using Boxed = $helper;") + w.wl + // mashalling + w.wl("static CppType toCpp(JsType j) { return _fromJs(j); }") + w.wl( + "static JsType fromCppOpt(const CppOptType& c) { return {_toJs(c)}; }" + ) + w.w("static JsType fromCpp(const CppType& c)").braced { + if (spec.cppNnType.isEmpty) { + w.wl(s"""::djinni::checkForNull(c.get(), "$helper::fromCpp");""") + } + w.wl("return fromCppOpt(c);") + } + w.wl + // method list + if (i.ext.cpp) { + w.wl("static em::val cppProxyMethods();") + } + w.wl + // stubs + if (i.ext.cpp) { + for (m <- i.methods.filter(m => !m.static || m.lang.js)) { + val selfRef = + if (m.static) "" + else if (m.params.isEmpty) "const CppType& self" + else "const CppType& self, " + w.w( + s"static ${stubRetType(m)} ${idCpp.method(m.ident)}(${selfRef}" + ) + w.w( + m.params + .map(p => { + s"${stubParamType(p.ty)} ${stubParamName(idCpp.local(p.ident))}" + }) + .mkString(",") + ) + w.wl(");") + } + w.wl + } + // js proxy + if (i.ext.js) { + w.w( + s"struct JsProxy: ::djinni::JsProxyBase, $cls, ::djinni::InstanceTracker<JsProxy>" + ).bracedSemi { + w.wl("JsProxy(const em::val& v) : JsProxyBase(v) {}") + for (m <- i.methods) { + if (!m.static) { + w.w( + s"${cppMarshal.fqReturnType(m.ret)} ${idCpp.method(m.ident)}(" + ) + w.w( + m.params + .map(p => { + s"${cppMarshal.fqParamType(p.ty)} ${idCpp.local(p.ident)}" + }) + .mkString(",") + ) + val constModifier = if (m.const) " const" else "" + w.wl(s")$constModifier override;") + } + } + } + } + // init consts + if (!spec.wasmOmitConstants && !i.consts.isEmpty) { + w.wl("static void staticInitializeConstants();"); + } + } + }), + (w => {}) + ) + + writeCppFileGeneric( + spec.wasmOutFolder.get, + helperNamespace(), + wasmFilenameStyle, + spec.wasmIncludePrefix + )( + ident.name, + origin, + refs.cpp, + (w => { + // method list + if (i.ext.cpp) { + w.w(s"em::val $helper::cppProxyMethods()").braced { + w.w( + "static const em::val methods = em::val::array(std::vector<std::string>" + ).bracedEnd(");") { + for (m <- i.methods) { + if (!m.static) { + w.wl(s""""${idJs.method(m.ident)}",""") + } + } + } + w.wl("return methods;") + } + } + w.wl + // stub methods + if (i.ext.cpp) { + for (m <- i.methods.filter(m => !m.static || m.lang.js)) { + val selfRef = + if (m.static) "" + else if (m.params.isEmpty) "const CppType& self" + else "const CppType& self, " + w.w( + s"${stubRetType(m)} $helper::${idCpp.method(m.ident)}(${selfRef}" + ) + w.w( + m.params + .map(p => { + s"${stubParamType(p.ty)} ${stubParamName(p.ident)}" + }) + .mkString(",") + ) + w.w(")").braced { + w.w("try").braced { + if (!m.ret.isEmpty) w.w("auto r = ") + if (m.static) w.w(s"$cls::") else w.w("self->") + writeAlignedCall( + w, + s"""${idCpp.method(m.ident)}(""", + m.params, + ")", + p => { + s"${helperClass(p.ty.resolved)}::toCpp(${stubParamName(p.ident)})" + } + ) + w.wl(";") + m.ret.fold(())(r => + w.wl(s"return ${helperClass(r.resolved)}::fromCpp(r);") + ) + } + w.w("catch(const std::exception& e)").braced { + val helper = + if (!m.ret.isEmpty) helperClass(m.ret.get.resolved) + else "void" + w.wl( + s"return ::djinni::ExceptionHandlingTraits<${helper}>::handleNativeException(e);" + ); + } + } + } + w.wl + } + // js proxy methods + if (i.ext.js) { + for (m <- i.methods) { + if (!m.static) { + val constModifier = if (m.const) " const" else "" + w.w( + s"${cppMarshal.fqReturnType(m.ret)} ${helper}::JsProxy::${idCpp + .method(m.ident)}(" + ) + w.w( + m.params + .map(p => { + s"${cppMarshal.fqParamType(p.ty)} ${idCpp.local(p.ident)}" + }) + .mkString(",") + ) + w.w(s")$constModifier").braced { + val methodName = + q(idJs.method(m.ident.name)) + (if (m.params.isEmpty) "" + else ", ") + writeAlignedCall( + w, + s"auto ret = callMethod($methodName", + m.params, + s")", + p => { + s"${helperClass(p.ty.resolved)}::fromCpp(${idCpp.local(p.ident)})" + } + ) + w.wl(";") + w.wl("checkError(ret);") + stubRetType(m) match { + case "void" => + case "em::val" => + w.wl( + s"return ${helperClass(m.ret.get.resolved)}::toCpp(ret);" + ) + case _ => + w.wl( + s"return ${helperClass(m.ret.get.resolved)}::toCpp(ret.as<${stubRetType(m)}>());" + ) + } + } + w.wl + } + } + } + + val fullyQualifiedName = withCppNamespace(ident.name) + val fullyQualifiedJsName = withWasmNamespace(idJs.ty(ident.name)) + + // embind + w.w(s"EMSCRIPTEN_BINDINGS(${fullyQualifiedName})").braced { + val classRegister = + if (!spec.wasmOmitNsAlias && !spec.wasmNamespace.isEmpty) { + s"""::djinni::DjinniClass_<$cls>("${fullyQualifiedJsName}", "${spec.wasmNamespace.get}.${idJs + .ty(ident.name)}")""" + } else { + s"""em::class_<$cls>("${fullyQualifiedJsName}")""" + } + + w.wl(classRegister).nested { + w.wl( + s""".smart_ptr<std::shared_ptr<$cls>>("${fullyQualifiedJsName}")""" + ) + w.wl(s""".function("${idJs + .method("native_destroy")}", &$helper::nativeDestroy)""") + if (i.ext.cpp) { + for (m <- i.methods.filter(m => !m.static || m.lang.js)) { + val funcType = if (m.static) "class_function" else "function" + w.wl(s""".$funcType("${idJs.method( + m.ident.name + )}", $helper::${idCpp.method(m.ident)})""") + } + } + w.wl(";") + } + } + // constants + if (!spec.wasmOmitConstants && !i.consts.isEmpty) { + generateWasmConstants(w, ident, i.consts); + } + }) + ) + } + + def withWasmNamespace(name: String, sep: String = "_") = + spec.wasmNamespace match { + case Some(p) => + p.replaceAll( + Pattern.quote("."), + Matcher.quoteReplacement(sep) + ) + sep + name + case None => name + } + + def withCppNamespace(name: String, sep: String = "_") = { + spec.cppNamespace.replaceAll( + "::", + Matcher.quoteReplacement(sep) + ) + sep + name + } + + override def generateRecord( + origin: String, + ident: Ident, + doc: Doc, + params: Seq[TypeParam], + r: Record + ) { + val refs = new WasmRefs(ident.name) + r.fields.foreach(f => refs.find(f.ty)) + r.consts.foreach(c => refs.find(c.ty)) + + val cls = withNs(Some(spec.cppNamespace), idCpp.ty(ident.name)) + val helper = helperClass(ident) + + writeHppFileGeneric( + spec.wasmOutFolder.get, + helperNamespace(), + wasmFilenameStyle + )( + ident.name, + origin, + refs.hpp, + Nil, + (w => { + w.wl(s"struct $helper").bracedSemi { + w.wl(s"using CppType = $cls;") + w.wl("using JsType = em::val;") + w.wl(s"using Boxed = $helper;") + w.wl + w.wl("static CppType toCpp(const JsType& j);") + w.wl("static JsType fromCpp(const CppType& c);") + // init consts + if (!spec.wasmOmitConstants && !r.consts.isEmpty) { + w.wl("static void staticInitializeConstants();"); + } + } + }), + (w => {}) + ) + + writeCppFileGeneric( + spec.wasmOutFolder.get, + helperNamespace(), + wasmFilenameStyle, + spec.wasmIncludePrefix + )( + ident.name, + origin, + refs.cpp, + (w => { + w.w(s"auto $helper::toCpp(const JsType& j) -> CppType").braced { + writeAlignedCall( + w, + "return {", + r.fields, + "}", + f => { + s"""${helperClass(f.ty.resolved)}::Boxed::toCpp(j["${idJs + .field(f.ident.name)}"])""" + } + ) + w.wl(";") + } + w.w(s"auto $helper::fromCpp(const CppType& c) -> JsType").braced { + w.wl("em::val js = em::val::object();") + for (f <- r.fields) { + w.wl(s"""js.set("${idJs.field(f.ident.name)}", ${helperClass( + f.ty.resolved + )}::Boxed::fromCpp(c.${idCpp.field(f.ident)}));""") + } + w.wl("return js;") + } + // constants + if (!spec.wasmOmitConstants && !r.consts.isEmpty) { + generateWasmConstants(w, ident, r.consts); + } + }) + ) + } +} diff --git a/src/main/scala/djinni/YamlGenerator.scala b/src/main/scala/djinni/YamlGenerator.scala index 187f19b8..8a172019 100644 --- a/src/main/scala/djinni/YamlGenerator.scala +++ b/src/main/scala/djinni/YamlGenerator.scala @@ -17,6 +17,8 @@ class YamlGenerator(spec: Spec) extends Generator(spec) { val javaMarshal = new JavaMarshal(spec) val jniMarshal = new JNIMarshal(spec) val cppCliMarshal = new CppCliMarshal(spec) + val wasmMarshal = new WasmGenerator(spec) + val tsMarshal = new TsGenerator(spec) case class QuotedString( str: String @@ -76,6 +78,8 @@ class YamlGenerator(spec: Spec) extends Generator(spec) { w.wl("java:").nested { write(w, java(td)) } w.wl("jni:").nested { write(w, jni(td)) } w.wl("cs:").nested { write(w, cs(td)) } + w.wl("wasm:").nested { write(w, wasm(td)) } + w.wl("ts:").nested { write(w, ts(td)) } } private def write(w: IndentWriter, m: Map[String, Any]): Unit = { @@ -122,7 +126,12 @@ class YamlGenerator(spec: Spec) extends Generator(spec) { def ext(e: Ext): String = (if (e.cpp) " +c" else "") + (if (e.objc) " +o" else "") + (if (e.java) " +j" - else "") + else + "") + (if ( + e.js + ) " +w" + else + "") def deriving(r: Record) = { if (r.derivingTypes.isEmpty) { "" @@ -188,6 +197,18 @@ class YamlGenerator(spec: Spec) extends Generator(spec) { "reference" -> cppCliMarshal.isReference(td) ) + private def wasm(td: TypeDecl) = Map[String, Any]( + "translator" -> QuotedString(wasmMarshal.helperName(mexpr(td))), + "header" -> QuotedString(wasmMarshal.include(td.ident)), + "typename" -> wasmMarshal.wasmType(mexpr(td)) + ) + + private def ts(td: TypeDecl) = Map[String, Any]( + "typename" -> tsMarshal.toTsType(mexpr(td), /*addNullability*/ false), + "module" -> QuotedString("./" + spec.tsModule) + // , "generic" -> false + ) + // TODO: there has to be a way to do all this without the MExpr/Meta conversions? private def mexpr(td: TypeDecl) = MExpr(meta(td), List()) @@ -251,7 +272,9 @@ object YamlGenerator { objcppOutRequired: Boolean, javaOutRequired: Boolean, jniOutRequired: Boolean, - cppCliOutRequired: Boolean + cppCliOutRequired: Boolean, + wasmOutRequired: Boolean, + tsOutRequired: Boolean ): MExtern = MExtern( td.ident.name.stripPrefix( td.properties("prefix").toString @@ -369,6 +392,16 @@ object YamlGenerator { "generic", _.asInstanceOf[Boolean] ) orElse Option.apply[Boolean](false) + ), + MExtern.Wasm( + getOptionalField(td, "wasm", "typename"), + getOptionalField(td, "wasm", "translator"), + getOptionalField(td, "wasm", "header") + ), + MExtern.Ts( + getOptionalField(td, "ts", "typename"), + getOptionalField(td, "ts", "module"), + getOptionalField(td, "ts", "generic", false) ) ) @@ -377,6 +410,33 @@ object YamlGenerator { m.asScala.collect { case (k: String, v: Any) => (k, v) } } } + + private def getOptionalField[T]( + td: ExternTypeDecl, + key: String, + subKey: String, + defVal: T + ) = { + if ((nested(td, key) getOrElse (Map[String, Any]())) contains subKey) + (nested(td, key) getOrElse (Map[String, Any]()))(subKey).asInstanceOf[T] + else defVal + } + + private def getOptionalField( + td: ExternTypeDecl, + key: String, + subKey: String + ) = { + try { + (nested(td, key) getOrElse (Map[String, Any]()))(subKey).toString + } catch { + case e: java.util.NoSuchElementException => { + println(s"Warning: in ${td.origin}, missing field $key/$subKey") + "[unspecified]" + } + } + } + private def nested[T]( td: ExternTypeDecl, isRequired: Boolean, @@ -389,7 +449,10 @@ object YamlGenerator { .flatten .map(v => convert(v)) match { case None if isRequired => - throw Error(td.ident.loc, s"missing '$lang' definitions").toException + throw Error( + td.ident.loc, + s"missing requried: $isRequired '$lang' definitions for $td, $lang, $key, $convert" + ).toException case other => other } } diff --git a/src/main/scala/djinni/ast/ast.scala b/src/main/scala/djinni/ast/ast.scala index f5ae3c3e..57b66f68 100644 --- a/src/main/scala/djinni/ast/ast.scala +++ b/src/main/scala/djinni/ast/ast.scala @@ -64,10 +64,11 @@ case class Ext( cpp: Boolean, objc: Boolean, py: Boolean, - cppcli: Boolean + cppcli: Boolean, + js: Boolean ) { def any(): Boolean = { - java || cpp || objc || py || cppcli + java || cpp || objc || py || cppcli || js } } @@ -119,7 +120,8 @@ object Interface { ret: Option[TypeRef], doc: Doc, static: Boolean, - const: Boolean + const: Boolean, + lang: Ext ) } diff --git a/src/main/scala/djinni/generator.scala b/src/main/scala/djinni/generator.scala index eed2053f..84e7688a 100644 --- a/src/main/scala/djinni/generator.scala +++ b/src/main/scala/djinni/generator.scala @@ -102,7 +102,19 @@ package object generatorTools { cWrapperIncludePrefix: String, cWrapperIncludeCppPrefix: String, pyImportPrefix: String, - cppJsonSerialization: Option[String] + cppJsonSerialization: Option[String], + wasmOutFolder: Option[File], + wasmIncludePrefix: String, + wasmIncludeCppPrefix: String, + wasmBaseLibIncludePrefix: String, + wasmOmitConstants: Boolean, + wasmNamespace: Option[String], + wasmOmitNsAlias: Boolean, + jsIdentStyle: JsIdentStyle, + tsOutFolder: Option[File], + tsModule: String, + tsSupportFilesOutFolder: Option[File], + moduleName: String ) def preComma(s: String): String = { @@ -168,6 +180,15 @@ package object generatorTools { const: IdentConverter, file: IdentConverter ) + case class JsIdentStyle( + ty: IdentConverter, + typeParam: IdentConverter, + method: IdentConverter, + field: IdentConverter, + local: IdentConverter, + enum: IdentConverter, + const: IdentConverter + ) object IdentStyle { val camelUpper: String => String = (s: String) => @@ -235,6 +256,16 @@ package object generatorTools { file = camelUpper ) + val jsDefault = JsIdentStyle( + ty = camelUpper, + typeParam = camelUpper, + method = camelLower, + field = camelLower, + local = camelLower, + enum = underCaps, + const = underCaps + ) + val styles: Map[String, String => String] = Map( "FooBar" -> camelUpper, "fooBar" -> camelLower, @@ -403,6 +434,18 @@ package object generatorTools { } new CffiGenerator(spec).generate(idl) } + if (spec.wasmOutFolder.isDefined) { + if (!spec.skipGeneration) { + createFolder("WASM", spec.wasmOutFolder.get) + } + new WasmGenerator(spec).generate(idl) + } + if (spec.tsOutFolder.isDefined) { + if (!spec.skipGeneration) { + createFolder("TypeScript", spec.tsOutFolder.get) + } + new TsGenerator(spec).generate(idl) + } None } catch { case GenerateException(message) => Some(message) @@ -530,6 +573,7 @@ abstract class Generator(spec: Spec) { val idObjc = spec.objcIdentStyle val idPython = spec.pyIdentStyle val idCs = spec.cppCliIdentStyle + val idJs = spec.jsIdentStyle def wrapNamespace( w: IndentWriter, @@ -716,26 +760,29 @@ abstract class Generator(spec: Spec) { def writeEnumOptionNone( w: IndentWriter, e: Enum, - ident: IdentConverter + ident: IdentConverter, + noneFlagWriter: () => String = () => " = 0," ): Unit = { - for ( - o <- e.options.find(_.specialFlag.contains(Enum.SpecialFlag.NoFlags)) - ) { + for (o <- e.options.find(_.specialFlag == Some(Enum.SpecialFlag.NoFlags))) { writeDoc(w, o.doc) - w.wl(ident(o.ident.name) + " = 0,") + w.wl(ident(o.ident.name) + noneFlagWriter()) } } def writeEnumOptions( w: IndentWriter, e: Enum, - ident: IdentConverter + ident: IdentConverter, + flagWriter: (Enum.Option, Int) => String = (o: Enum.Option, shift: Int) => + s" = 1u << $shift,", + ordinalWriter: (Int) => String = (ordinal: Int) => "," ): Unit = { var shift = 0 for (o <- normalEnumOptions(e)) { writeDoc(w, o.doc) w.wl( - ident(o.ident.name) + (if (e.flags) s" = 1u << $shift" else "") + "," + ident(o.ident.name) + (if (e.flags) flagWriter(o, shift) + else ordinalWriter(shift)) ) shift += 1 } @@ -744,19 +791,20 @@ abstract class Generator(spec: Spec) { def writeEnumOptionAll( w: IndentWriter, e: Enum, - ident: IdentConverter + ident: IdentConverter, + allFlagWriter: (Seq[Tuple2[Int, String]]) => String = + (ordinalsAndNames: Seq[Tuple2[Int, String]]) => + s""" = ${ordinalsAndNames + .map(e => e._2) + .fold("0")((acc, o) => acc + " | " + o)},""" ): Unit = { for ( o <- e.options.find(_.specialFlag.contains(Enum.SpecialFlag.AllFlags)) ) { writeDoc(w, o.doc) - w.w(ident(o.ident.name) + " = ") - w.w( - normalEnumOptions(e) - .map(o => ident(o.ident.name)) - .fold("0")((acc, o) => acc + " | " + o) - ) - w.wl(",") + val ordinalsAndNames = normalEnumOptions(e).zipWithIndex + .map { case (o, i) => Tuple2(i, ident(o.ident.name)) } + w.wl(ident(o.ident.name) + allFlagWriter(ordinalsAndNames)) } } diff --git a/src/main/scala/djinni/meta.scala b/src/main/scala/djinni/meta.scala index b8752d5c..ce17f84c 100644 --- a/src/main/scala/djinni/meta.scala +++ b/src/main/scala/djinni/meta.scala @@ -44,7 +44,9 @@ package object meta { objcpp: MExtern.Objcpp, java: MExtern.Java, jni: MExtern.Jni, - cs: MExtern.Cs + cs: MExtern.Cs, + wasm: MExtern.Wasm, + ts: MExtern.Ts ) extends Meta object MExtern { // These hold the information marshals need to interface with existing types correctly @@ -112,6 +114,16 @@ package object meta { Boolean ] // Set to false to exclude type arguments from the C++/CLI typename. This is false by default. Useful if template arguments are only used in C++. ) + case class Wasm( + typename: String, // The Emscripten type to use (e.g. em::val, int32_t) + translator: String, // C++ typename containing toCpp/fromCpp methods + header: String // Where to find the translator class + ) + case class Ts( + typename: String, // The TypeScript type + module: String, // The module to import for the type + generic: Boolean + ) } abstract sealed class MOpaque extends Meta { val idlName: String } @@ -270,6 +282,10 @@ package object meta { } } + def isOptional(ty: MExpr): Boolean = { + ty.base == MOptional && ty.args.length == 1 + } + def isOptionalInterface(ty: MExpr): Boolean = { ty.base == MOptional && ty.args.length == 1 && isInterface(ty.args.head) } diff --git a/src/main/scala/djinni/parser.scala b/src/main/scala/djinni/parser.scala index d7fc5449..73d63338 100644 --- a/src/main/scala/djinni/parser.scala +++ b/src/main/scala/djinni/parser.scala @@ -94,10 +94,34 @@ case class Parser(includePaths: List[String]) { def ext(default: Ext): Parser[Ext] = (rep1("+" ~> ident) >> checkExts) | success(default) def extRecord: Parser[Ext] = ext( - Ext(java = false, cpp = false, objc = false, py = false, cppcli = false) + Ext( + java = false, + cpp = false, + objc = false, + py = false, + cppcli = false, + js = false + ) ) def extInterface: Parser[Ext] = ext( - Ext(java = true, cpp = true, objc = true, py = true, cppcli = true) + Ext( + java = true, + cpp = true, + objc = true, + py = true, + cppcli = true, + js = true + ) + ) + def supportLang: Parser[Ext] = ext( + Ext( + java = true, + cpp = true, + objc = true, + py = true, + cppcli = true, + js = true + ) ) def checkExts(parts: List[Ident]): Parser[Ext] = { @@ -106,6 +130,7 @@ case class Parser(includePaths: List[String]) { var foundObjc = false var foundPy = false var foundCs = false + var foundJavascript = false for (part <- parts) part.name match { @@ -129,9 +154,15 @@ case class Parser(includePaths: List[String]) { if (foundCs) return err("Found multiple \"s\" modifiers.") foundCs = true } + case "w" => { + if (foundJavascript) return err("Found multiple \"w\" modifiers.") + foundJavascript = true + } case _ => return err("Invalid modifier \"" + part.name + "\"") } - success(Ext(foundJava, foundCpp, foundObjc, foundPy, foundCs)) + success( + Ext(foundJava, foundCpp, foundObjc, foundPy, foundCs, foundJavascript) + ) } def typeDef: Parser[TypeDef] = record | enum | flags | interface @@ -224,10 +255,28 @@ case class Parser(includePaths: List[String]) { def method: Parser[Interface.Method] = doc ~ staticLabel ~ constLabel ~ ident ~ parens( repsepend(field, ",") - ) ~ opt(ret) ^^ { - case doc ~ staticLabel ~ constLabel ~ ident ~ params ~ ret => - Interface.Method(ident, params, ret, doc, staticLabel, constLabel) + ) ~ opt(ret) ~ supportLang ^^ { + case doc ~ staticLabel ~ constLabel ~ ident ~ params ~ ret ~ ext => + Interface.Method( + ident, + params, + ret, + doc, + staticLabel, + constLabel, + ext + ) } + // def method: Parser[Interface.Method] = + // doc ~ staticLabel ~ constLabel ~ ident ~ parens(repsepend(field, ",")) ~ opt(ret) ~ supportLang ^^ { + // case doc ~ staticLabel ~ constLabel ~ ident ~ params ~ ret ~ ext => { + // ret match { + // case Some(r) if (r.expr.ident.name == "void") => Interface.Method(ident, params, None, doc, staticLabel, constLabel, ext) + // case _ => Interface.Method(ident, params, ret, doc, staticLabel, constLabel, ext) + // } + // } + // } + def ret: Parser[TypeRef] = ":" ~> typeRef def boolValue: Parser[Boolean] = "([Tt]rue)|([Ff]alse)".r ^^ { s: String => diff --git a/src/main/scala/djinni/resolver.scala b/src/main/scala/djinni/resolver.scala index 6eac5aad..4ddba2aa 100644 --- a/src/main/scala/djinni/resolver.scala +++ b/src/main/scala/djinni/resolver.scala @@ -36,7 +36,9 @@ package object resolver { objcppOutRequired: Boolean, javaOutRequired: Boolean, jniOutRequired: Boolean, - cppCliOutRequired: Boolean + cppCliOutRequired: Boolean, + wasmOutRequired: Boolean, + tsOutRequired: Boolean ): Option[Error] = { try { @@ -77,7 +79,9 @@ package object resolver { objcppOutRequired = objcppOutRequired, javaOutRequired = javaOutRequired, jniOutRequired = jniOutRequired, - cppCliOutRequired = cppCliOutRequired + cppCliOutRequired = cppCliOutRequired, + wasmOutRequired = wasmOutRequired, + tsOutRequired = tsOutRequired ) } )