Browse Source

Implement support for properties in Emscripten generator. (#1932)

* Implement support for properties in Emscripten generator.

* CLI: Fix option validation to check for valid Emscripten platform.

* Ignore functions returning tag types for now in JS generation.

* Use a Lua bindings spec file for Emscripten tests.

* Improve JS testing scripts.

* Only run packing step for release CI builds.
main
João Matos 2 months ago committed by GitHub
parent
commit
e093f713b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 7
      .github/workflows/main.yml
  2. 7
      src/CLI/Generator.cs
  3. 14
      src/Generator/Driver.cs
  4. 31
      src/Generator/Generators/Emscripten/EmscriptenSources.cs
  5. 2
      src/Generator/Options.cs
  6. 8
      src/Generator/Passes/CheckIgnoredDecls.cs
  7. 20
      tests/Classes.h
  8. 23
      tests/emscripten/bindings.lua
  9. 9
      tests/emscripten/test.mjs
  10. 76
      tests/emscripten/test.sh
  11. 64
      tests/napi/test.sh
  12. 73
      tests/quickjs/test.sh
  13. 2
      tests/test.sh
  14. 58
      tests/ts/test.sh

7
.github/workflows/main.yml

@ -37,7 +37,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup emsdk - name: Setup emsdk
uses: mymindstorm/setup-emsdk@v11 uses: mymindstorm/setup-emsdk@v14
with: with:
version: ${{ env.EMSCRIPTEN_VERSION }} version: ${{ env.EMSCRIPTEN_VERSION }}
actions-cache-folder: emsdk-cache-${{ runner.os }} actions-cache-folder: emsdk-cache-${{ runner.os }}
@ -88,14 +88,15 @@ jobs:
- name: Test (QuickJS) - name: Test (QuickJS)
if: runner.os != 'Windows' if: runner.os != 'Windows'
shell: bash shell: bash
run: tests/quickjs/test.sh -dotnet_configuration $BUILD_CONFIGURATION run: tests/quickjs/test.sh --dotnet-config $BUILD_CONFIGURATION
- name: Test (Emscripten) - name: Test (Emscripten)
if: runner.os != 'Windows' if: runner.os != 'Windows'
shell: bash shell: bash
run: tests/emscripten/test.sh -dotnet_configuration $BUILD_CONFIGURATION run: tests/emscripten/test.sh --dotnet-config $BUILD_CONFIGURATION
- name: Pack - name: Pack
if: matrix.build-cfg == 'Release'
shell: bash shell: bash
run: build/build.sh prepack -platform $PLATFORM -configuration $BUILD_CONFIGURATION run: build/build.sh prepack -platform $PLATFORM -configuration $BUILD_CONFIGURATION

7
src/CLI/Generator.cs

@ -90,6 +90,13 @@ namespace CppSharp
options.Platform ??= Platform.Host; options.Platform ??= Platform.Host;
if (options.Architecture is TargetArchitecture.WASM32 or TargetArchitecture.WASM64 &&
options.Platform is not TargetPlatform.Emscripten)
{
messages.Add("Please set Emscripten platform for WASM architectures.");
return false;
}
if (string.IsNullOrEmpty(options.OutputDir)) if (string.IsNullOrEmpty(options.OutputDir))
{ {
options.OutputDir = Path.Combine(Directory.GetCurrentDirectory(), "gen"); options.OutputDir = Path.Combine(Directory.GetCurrentDirectory(), "gen");

14
src/Generator/Driver.cs

@ -252,7 +252,12 @@ namespace CppSharp
passes.AddPass(new CleanInvalidDeclNamesPass()); passes.AddPass(new CleanInvalidDeclNamesPass());
passes.AddPass(new FastDelegateToDelegatesPass()); passes.AddPass(new FastDelegateToDelegatesPass());
passes.AddPass(new FieldToPropertyPass());
if (Options.GeneratorKind != GeneratorKind.Emscripten)
{
passes.AddPass(new FieldToPropertyPass());
}
passes.AddPass(new CheckIgnoredDeclsPass()); passes.AddPass(new CheckIgnoredDeclsPass());
passes.AddPass(new CheckEnumsPass()); passes.AddPass(new CheckEnumsPass());
passes.AddPass(new MakeProtectedNestedTypesPublicPass()); passes.AddPass(new MakeProtectedNestedTypesPublicPass());
@ -283,9 +288,14 @@ namespace CppSharp
passes.AddPass(new CheckDuplicatedNamesPass()); passes.AddPass(new CheckDuplicatedNamesPass());
if (Options.IsCLIGenerator || Options.IsCSharpGenerator) if (Options.IsCLIGenerator || Options.IsCSharpGenerator
|| Options.GeneratorKind.ID is GeneratorKind.Emscripten_ID)
{ {
passes.RenameDeclsUpperCase(RenameTargets.Any & ~RenameTargets.Parameter); passes.RenameDeclsUpperCase(RenameTargets.Any & ~RenameTargets.Parameter);
}
if (Options.IsCLIGenerator || Options.IsCSharpGenerator)
{
passes.AddPass(new CheckKeywordNamesPass()); passes.AddPass(new CheckKeywordNamesPass());
} }

31
src/Generator/Generators/Emscripten/EmscriptenSources.cs

@ -117,11 +117,6 @@ namespace CppSharp.Generators.Emscripten
} }
} }
public override bool VisitProperty(Property property)
{
return true;
}
public override bool VisitMethodDecl(Method method) public override bool VisitMethodDecl(Method method)
{ {
Indent(); Indent();
@ -130,9 +125,33 @@ namespace CppSharp.Generators.Emscripten
return ret; return ret;
} }
public override bool VisitProperty(Property property)
{
if (property.Field != null)
return property.Field.Visit(this);
if (!property.GetMethod.IsConst)
{
Console.WriteLine($"Cannot bind non-const property getter method: {property.GetMethod.QualifiedOriginalName}");
return false;
}
var @class = property.Namespace as Class;
Indent();
Write($".property(\"{property.Name}\", &{@class.QualifiedOriginalName}::{property.GetMethod.OriginalName}");
if (property.HasSetter)
Write($", &{@class.QualifiedOriginalName}::{property.SetMethod.OriginalName}");
WriteLine(")");
Unindent();
return true;
}
public override bool VisitFieldDecl(Field field) public override bool VisitFieldDecl(Field field)
{ {
WriteLineIndent($".field(\"{field.Name}\", &{field.Class.QualifiedOriginalName}::{field.OriginalName})"); WriteLineIndent($".property(\"{field.Name}\", &{field.Class.QualifiedOriginalName}::{field.OriginalName})");
return true; return true;
} }

2
src/Generator/Options.cs

@ -205,6 +205,8 @@ namespace CppSharp
public bool IsCLIGenerator => GeneratorKind == GeneratorKind.CLI; public bool IsCLIGenerator => GeneratorKind == GeneratorKind.CLI;
public bool IsJSGenerator => GeneratorKind == GeneratorKind.Emscripten || GeneratorKind == GeneratorKind.QuickJS;
public readonly List<string> DependentNameSpaces = new List<string>(); public readonly List<string> DependentNameSpaces = new List<string>();
public bool MarshalCharAsManagedChar { get; set; } public bool MarshalCharAsManagedChar { get; set; }
public bool MarshalConstCharArrayAsString { get; set; } = true; public bool MarshalConstCharArrayAsString { get; set; } = true;

8
src/Generator/Passes/CheckIgnoredDecls.cs

@ -184,6 +184,14 @@ namespace CppSharp.Passes
return false; return false;
} }
if (Options.IsJSGenerator && function is Method { Kind: CXXMethodKind.Normal } && ret.Type.GetFinalPointee().IsClass())
{
function.ExplicitlyIgnore();
Diagnostics.Debug("Function '{0}' was ignored due to {1} return decl not yet implemented in JS generators",
function.Name, msg);
return false;
}
foreach (var param in function.Parameters) foreach (var param in function.Parameters)
{ {
if (HasInvalidDecl(param, out msg)) if (HasInvalidDecl(param, out msg))

20
tests/Classes.h

@ -6,8 +6,8 @@ class Class
{ {
public: public:
void ReturnsVoid() {} void ReturnsVoid() {}
int ReturnsInt() { return 0; } int ReturnsInt() const { return 0; }
// Class* PassAndReturnsClassPtr(Class* obj) { return obj; } Class* PassAndReturnsClassPtr(Class* obj) { return obj; }
}; };
class ClassWithField class ClassWithField
@ -18,7 +18,21 @@ public:
{ {
} }
int Field; int Field;
int ReturnsField() { return Field; } int ReturnsField() const { return Field; }
};
class ClassWithProperty
{
public:
ClassWithProperty()
: Field(10)
{
}
int GetField() const { return Field; }
void SetField(int value) { Field = value; }
private:
int Field;
}; };
class ClassWithOverloads class ClassWithOverloads

23
tests/emscripten/bindings.lua

@ -0,0 +1,23 @@
generator "emscripten"
platform "emscripten"
architecture "wasm32"
includedirs
{
"..",
"../../include",
}
output "gen"
module "tests"
namespace "test"
headers
{
"Builtins.h",
"Classes.h",
"Classes2.h",
"Delegates.h",
"Enums.h",
"Overloads.h"
}

9
tests/emscripten/test.mjs

@ -111,7 +111,14 @@ function classes() {
var classWithField = new test.ClassWithField(); var classWithField = new test.ClassWithField();
eq(classWithField.ReturnsField(), 10); eq(classWithField.ReturnsField(), 10);
//eq(classWithField.Field, 10); eq(classWithField.Field, 10);
classWithField.Field = 20;
eq(classWithField.ReturnsField(), 20);
var classWithProperty = new test.ClassWithProperty();
eq(classWithProperty.Field, 10);
classWithProperty.Field = 20;
eq(classWithProperty.Field, 20);
} }
builtins(); builtins();

76
tests/emscripten/test.sh

@ -1,27 +1,46 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -e set -e
dir=$(cd "$(dirname "$0")"; pwd) dir=$(cd "$(dirname "$0")"; pwd)
rootdir="$dir/../.." rootdir="$dir/../.."
dotnet_configuration=Release dotnet_configuration=DebugOpt
configuration=debug make_configuration=debug
platform=x64 platform=x64
jsinterp=$(which node) jsinterp=$(which node)
for arg in "$@"; do usage() {
case $arg in cat <<EOF
--with-node=*) Usage: $(basename $0) [--with-node=NODE] [--make-config CONFIG] [--dotnet-config CONFIG]
jsinterp="${arg#*=}" EOF
shift exit 1
;; }
-configuration)
configuration=$2 while [[ $# -gt 0 ]]; do
shift case "$1" in
;; --with-node=*)
-dotnet_configuration) jsinterp="${1#*=}"
dotnet_configuration=$2 shift
shift ;;
;; --with-node)
esac jsinterp="$2"
shift 2
;;
--make-config|--make-configuration)
make_configuration="$2"
shift 2
;;
--dotnet-config|--dotnet-configuration)
dotnet_configuration="$2"
shift 2
;;
-h|--help)
usage
;;
*)
echo "Unknown option: $1" >&2
usage
;;
esac
done done
if [ "$CI" = "true" ]; then if [ "$CI" = "true" ]; then
@ -34,20 +53,21 @@ else
reset=`tput sgr0` reset=`tput sgr0`
fi fi
# 1) Generate
generate=true generate=true
if [ $generate = true ]; then if [ $generate = true ]; then
echo "${green}Generating bindings${reset}" echo "${green}Generating bindings with .NET configuration $dotnet_configuration${reset}"
dotnet $rootdir/bin/${dotnet_configuration}/CppSharp.CLI.dll \ dotnet "$rootdir/bin/${dotnet_configuration}/CppSharp.CLI.dll" --property=keywords \
--gen=emscripten --platform=emscripten --arch=wasm32 \ "$dir/bindings.lua"
-I$dir/.. -I$rootdir/include -o $dir/gen -m tests $dir/../*.h
fi fi
echo "${green}Building generated binding files${reset}" # 2) Build
premake=$rootdir/build/premake.sh echo "${green}Building generated binding files (make config: $make_configuration)${reset}"
config=$configuration $premake --file=$dir/premake5.lua gmake premake="$rootdir/build/premake.sh"
emmake make -C $dir/gen "$premake" --file=$dir/premake5.lua gmake2
echo config=$make_configuration emmake make -C "$dir/gen"
# 3) Test
echo
echo "${green}Executing JS tests with Node${reset}" echo "${green}Executing JS tests with Node${reset}"
$jsinterp $dir/test.mjs "$jsinterp" "$dir/test.mjs"

64
tests/napi/test.sh

@ -1,9 +1,47 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -e set -e
dir=$(cd "$(dirname "$0")"; pwd) dir=$(cd "$(dirname "$0")"; pwd)
rootdir="$dir/../.." rootdir="$dir/../.."
configuration=Release dotnet_configuration=DebugOpt
make_configuration=debug
platform=x64 platform=x64
jsinterp=$(which node)
usage() {
cat <<EOF
Usage: $(basename $0) [--with-node=NODE] [--make-config CONFIG] [--dotnet-config CONFIG]
EOF
exit 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
--with-node=*)
jsinterp="${1#*=}"
shift
;;
--with-node)
jsinterp="$2"
shift 2
;;
--make-config|--make-configuration)
make_configuration="$2"
shift 2
;;
--dotnet-config|--dotnet-configuration)
dotnet_configuration="$2"
shift 2
;;
-h|--help)
usage
;;
*)
echo "Unknown option: $1" >&2
usage
;;
esac
done
if [ "$CI" = "true" ]; then if [ "$CI" = "true" ]; then
red="" red=""
@ -15,15 +53,21 @@ else
reset=`tput sgr0` reset=`tput sgr0`
fi fi
echo "${green}Generating bindings${reset}" # 1) Generate
dotnet $rootdir/bin/${configuration}/CppSharp.CLI.dll \ generate=true
--gen=napi -I$dir/.. -o $dir/gen -m tests $dir/../*.h if [ $generate = true ]; then
echo "${green}Generating bindings with .NET configuration $dotnet_configuration${reset}"
dotnet "$rootdir/bin/${dotnet_configuration}/CppSharp.CLI.dll" \
--gen=napi -I$dir/.. -o $dir/gen -m tests $dir/../*.h
fi
echo "${green}Building generated binding files${reset}" # 2) Build
premake=$rootdir/build/premake.sh echo "${green}Building generated binding files (make config: $make_configuration)${reset}"
$premake --file=$dir/premake5.lua gmake premake="$rootdir/build/premake.sh"
make -C $dir/gen "$premake" --file=$dir/premake5.lua gmake
echo config=$make_configuration make -C "$dir/gen"
# 3) Test
echo
echo "${green}Executing JS tests with Node${reset}" echo "${green}Executing JS tests with Node${reset}"
node $dir/test.js "$jsinterp" "$dir/test.js"

73
tests/quickjs/test.sh

@ -1,25 +1,47 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -e set -e
dir=$(cd "$(dirname "$0")"; pwd) dir=$(cd "$(dirname "$0")"; pwd)
rootdir="$dir/../.." rootdir="$dir/../.."
dotnet_configuration=Release dotnet_configuration=DebugOpt
configuration=debug make_configuration=debug
platform=x64
jsinterp="$dir/runtime/build/qjs" jsinterp="$dir/runtime/build/qjs"
for arg in "$@"; do usage() {
case $arg in cat <<EOF
-configuration) Usage: $(basename $0) [--with-node=NODE] [--make-config CONFIG] [--dotnet-config CONFIG]
configuration=$2 EOF
shift exit 1
;; }
-dotnet_configuration)
dotnet_configuration=$2
shift
;;
esac
done
cd $dir while [[ $# -gt 0 ]]; do
case "$1" in
--with-node=*)
jsinterp="${1#*=}"
shift
;;
--with-node)
jsinterp="$2"
shift 2
;;
--make-config|--make-configuration)
make_configuration="$2"
shift 2
;;
--dotnet-config|--dotnet-configuration)
dotnet_configuration="$2"
shift 2
;;
-h|--help)
usage
;;
*)
echo "Unknown option: $1" >&2
usage
;;
esac
done
if [ "$CI" = "true" ]; then if [ "$CI" = "true" ]; then
red="" red=""
@ -31,20 +53,21 @@ else
reset=`tput sgr0` reset=`tput sgr0`
fi fi
# 1) Generate
generate=true generate=true
if [ $generate = true ]; then if [ $generate = true ]; then
echo "${green}Generating bindings${reset}" echo "${green}Generating bindings with .NET configuration $dotnet_configuration${reset}"
dotnet $rootdir/bin/${dotnet_configuration}/CppSharp.CLI.dll \ dotnet "$rootdir/bin/${dotnet_configuration}/CppSharp.CLI.dll" "$dir/bindings.lua"
$dir/bindings.lua
fi fi
echo "${green}Building generated binding files${reset}" # 2) Build
premake=$rootdir/build/premake.sh echo "${green}Building generated binding files (make config: $make_configuration)${reset}"
config=$configuration $premake --file=$dir/premake5.lua gmake2 premake="$rootdir/build/premake.sh"
verbose=true make -C $dir/gen "$premake" --file=$dir/premake5.lua gmake2
echo config=$make_configuration verbose=true make -C "$dir/gen"
# 3) Test
echo
echo "${green}Executing JS tests with QuickJS${reset}" echo "${green}Executing JS tests with QuickJS${reset}"
cp $dir/gen/bin/$configuration/libtest.so $dir cp $dir/gen/bin/$make_configuration/libtest.so $dir
$jsinterp --std $dir/test.js $jsinterp --std $dir/test.js

2
tests/test.sh

@ -2,6 +2,6 @@
set -e set -e
dir=$(cd "$(dirname "$0")"; pwd) dir=$(cd "$(dirname "$0")"; pwd)
$dir/napi/test.sh # $dir/napi/test.sh
$dir/quickjs/test.sh $dir/quickjs/test.sh
$dir/emscripten/test.sh $dir/emscripten/test.sh

58
tests/ts/test.sh

@ -1,11 +1,47 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -e set -e
dir=$(cd "$(dirname "$0")"; pwd) dir=$(cd "$(dirname "$0")"; pwd)
rootdir="$dir/../.." rootdir="$dir/../.."
dotnet_configuration=Release dotnet_configuration=DebugOpt
configuration=debug make_configuration=debug
platform=x64 platform=x64
jsinterp="$rootdir/deps/quickjs/qjs-debug" jsinterp="$dir/runtime/build/qjs"
usage() {
cat <<EOF
Usage: $(basename $0) [--with-node=NODE] [--make-config CONFIG] [--dotnet-config CONFIG]
EOF
exit 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
--with-node=*)
jsinterp="${1#*=}"
shift
;;
--with-node)
jsinterp="$2"
shift 2
;;
--make-config|--make-configuration)
make_configuration="$2"
shift 2
;;
--dotnet-config|--dotnet-configuration)
dotnet_configuration="$2"
shift 2
;;
-h|--help)
usage
;;
*)
echo "Unknown option: $1" >&2
usage
;;
esac
done
if [ "$CI" = "true" ]; then if [ "$CI" = "true" ]; then
red="" red=""
@ -17,22 +53,14 @@ else
reset=`tput sgr0` reset=`tput sgr0`
fi fi
# 1) Generate
generate=true generate=true
if [ $generate = true ]; then if [ $generate = true ]; then
echo "${green}Generating bindings${reset}" echo "${green}Generating bindings with .NET configuration $dotnet_configuration${reset}"
dotnet $rootdir/bin/${dotnet_configuration}/CppSharp.CLI.dll \ dotnet "$rootdir/bin/${dotnet_configuration}/CppSharp.CLI.dll" \
--gen=ts -I$dir/.. -I$rootdir/include -o $dir/gen -m tests $dir/../*.h --gen=ts -I$dir/.. -I$rootdir/include -o $dir/gen -m tests $dir/../*.h
fi fi
echo "${green}Building generated binding files${reset}" # 2) Type-checking
#make -C $dir/gen
echo
echo "${green}Typechecking generated binding files with tsc${reset}" echo "${green}Typechecking generated binding files with tsc${reset}"
#tsc --noEmit --strict --noImplicitAny --strictNullChecks --strictFunctionTypes --noImplicitThis gen/*.d.ts #tsc --noEmit --strict --noImplicitAny --strictNullChecks --strictFunctionTypes --noImplicitThis gen/*.d.ts
# echo "${green}Executing JS tests with QuickJS${reset}"
# cp $dir/gen/bin/$configuration/libtest.so $dir
# #cp $dir/gen/bin/$configuration/libtest.dylib $dir
# $jsinterp --std $dir/test.js
Loading…
Cancel
Save