From a2fb3762317aa85e1e7c408f6280975d6cde3345 Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Tue, 23 Apr 2024 16:01:26 +0800 Subject: [PATCH] feat: add csharp interop code generation but no test. - add csharp interop code generation without testing. - add annotation for bmap binding project. - fix README. --- CodeGen/BMapBindings/CSharpWriter.java | 277 +++++++++++++++++++++- CodeGen/BMapBindings/ExpFctDecl.java | 21 +- CodeGen/BMapBindings/ExpFctParamDecl.java | 38 ++- CodeGen/BMapBindings/PythonWriter.java | 20 +- CodeGen/BMapBindings/VariableType.java | 84 ++++++- README.md | 6 +- 6 files changed, 421 insertions(+), 25 deletions(-) diff --git a/CodeGen/BMapBindings/CSharpWriter.java b/CodeGen/BMapBindings/CSharpWriter.java index ee0480c..9e973f4 100644 --- a/CodeGen/BMapBindings/CSharpWriter.java +++ b/CodeGen/BMapBindings/CSharpWriter.java @@ -1,12 +1,283 @@ import java.io.OutputStreamWriter; import java.util.Vector; +import java.util.stream.Collectors; public class CSharpWriter { - + + /** + * The class represent the C# type corresponding to extracted variable type. + */ + private static class CsInteropType { + public CsInteropType() { + mMarshalAs = null; + mCsType = null; + } + + /** + * The argument of MarshalAsAttribute constructor. In generation, this field + * should be used like this: "[MarshalAs(THIS)]" (for parameter) or "[return: + * MarshalAs(THIS)]" (for return value). + */ + public String mMarshalAs; + /** + * The C# type used in interop function declaration for corresponding parameter. + */ + public String mCsType; + } + + /** + * C# specified function which get C# used interop MarshalAs constructor + * arguments and C# type used in interop function declaration. + * + * @param vt The instance of {@linkplain VariableType} for fetching interop + * type. + * @return The corresponding interop type of given variable type. + */ + private static CsInteropType getCsInteropType(ExpFctParamDecl paramdecl) { + // get essential variable type properties first + VariableType vt = paramdecl.mVarType; + String vt_base_type = vt.getBaseType(); + int vt_pointer_level = vt.getPointerLevel(); + + // create return value + CsInteropType ret = new CsInteropType(); + + // use "switch" to check variable type + switch (vt_base_type) { + case "CKSTRING": + // only allow 0 and 1 pointer level for string. + switch (vt_pointer_level) { + case 0: + ret.mMarshalAs = "UnmanagedType.LPUTF8Str"; + ret.mCsType = "string"; + break; + case 1: + ret.mMarshalAs = "UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(BMStringArrayMashaler)"; + ret.mCsType = "string[]"; + break; + } + break; + case "CKDWORD": + if (vt_pointer_level == 0) { + ret.mMarshalAs = "UnmanagedType.U4"; + ret.mCsType = "uint"; + } else { + ret.mMarshalAs = "UnmanagedType.SysInt"; + ret.mCsType = "IntPtr"; + } + break; + case "CKWORD": + if (vt_pointer_level == 0) { + ret.mMarshalAs = "UnmanagedType.U2"; + ret.mCsType = "ushort"; + } else { + ret.mMarshalAs = "UnmanagedType.SysInt"; + ret.mCsType = "IntPtr"; + } + break; + case "CKINT": + if (vt_pointer_level == 0) { + ret.mMarshalAs = "UnmanagedType.I4"; + ret.mCsType = "int"; + } else { + ret.mMarshalAs = "UnmanagedType.SysInt"; + ret.mCsType = "IntPtr"; + } + break; + case "bool": + if (vt_pointer_level == 0) { + ret.mMarshalAs = "UnmanagedType.U1"; + ret.mCsType = "bool"; + } else { + ret.mMarshalAs = "UnmanagedType.SysInt"; + ret.mCsType = "IntPtr"; + } + break; + case "CKFLOAT": + if (vt_pointer_level == 0) { + ret.mMarshalAs = "UnmanagedType.R4"; + ret.mCsType = "float"; + } else { + ret.mMarshalAs = "UnmanagedType.SysInt"; + ret.mCsType = "IntPtr"; + } + break; + case "CKBYTE": + if (vt_pointer_level == 0) { + ret.mMarshalAs = "UnmanagedType.U1"; + ret.mCsType = "byte"; + } else { + ret.mMarshalAs = "UnmanagedType.SysInt"; + ret.mCsType = "IntPtr"; + } + case "CK_ID": + if (vt_pointer_level == 0) { + ret.mMarshalAs = "UnmanagedType.U4"; + ret.mCsType = "uint"; + } else { + ret.mMarshalAs = "UnmanagedType.SysInt"; + ret.mCsType = "IntPtr"; + } + break; + case "NakedOutputCallback": + // callback actually is a function pointer + // so it only allow base type without any pointer level. + if (vt_pointer_level == 0) { + ret.mMarshalAs = "UnmanagedType.FunctionPtr"; + ret.mCsType = "OutputCallback"; + } + break; + case "BMFile": + // In any case, BMFile only should be raw pointer + if (vt_pointer_level != 0) { + ret.mMarshalAs = "UnmanagedType.SysInt"; + ret.mCsType = "IntPtr"; + } + break; + case "BMMeshTransition": + // In any case, BMMeshTransition only should be raw pointer + if (vt_pointer_level != 0) { + ret.mMarshalAs = "UnmanagedType.SysInt"; + ret.mCsType = "IntPtr"; + } + break; + case "VxVector3": + if (vt_pointer_level == 0) { + ret.mMarshalAs = "UnmanagedType.Struct"; + ret.mCsType = "VxVector3"; + } else { + ret.mMarshalAs = "UnmanagedType.SysInt"; + ret.mCsType = "IntPtr"; + } + break; + case "VxVector2": + if (vt_pointer_level == 0) { + ret.mMarshalAs = "UnmanagedType.Struct"; + ret.mCsType = "VxVector2"; + } else { + ret.mMarshalAs = "UnmanagedType.SysInt"; + ret.mCsType = "IntPtr"; + } + break; + case "VxColor": + if (vt_pointer_level == 0) { + ret.mMarshalAs = "UnmanagedType.Struct"; + ret.mCsType = "VxColor"; + } else { + ret.mMarshalAs = "UnmanagedType.SysInt"; + ret.mCsType = "IntPtr"; + } + break; + case "VxMatrix": + if (vt_pointer_level == 0) { + ret.mMarshalAs = "UnmanagedType.Struct"; + ret.mCsType = "VxMatrix"; + } else { + ret.mMarshalAs = "UnmanagedType.SysInt"; + ret.mCsType = "IntPtr"; + } + break; + case "CK_TEXTURE_SAVEOPTIONS": + case "VX_PIXELFORMAT": + case "VXTEXTURE_BLENDMODE": + case "VXTEXTURE_FILTERMODE": + case "VXTEXTURE_ADDRESSMODE": + case "VXBLEND_MODE": + case "VXFILL_MODE": + case "VXSHADE_MODE": + case "VXCMPFUNC": + case "VXMESH_LITMODE": + // all enum share the same underlying type. + if (vt_pointer_level == 0) { + ret.mMarshalAs = "UnmanagedType.U4"; + ret.mCsType = "uint"; + } else { + ret.mMarshalAs = "UnmanagedType.SysInt"; + ret.mCsType = "IntPtr"; + } + break; + } + + // check whether we successfully get result + if (ret.mMarshalAs == null || ret.mCsType == null) { + throw new IllegalArgumentException("Unexpected type: " + vt.toCType()); + } + + // return value + return ret; + } + public static void writeCSharpCode(Vector data) throws Exception { OutputStreamWriter writer = CommonHelper.openWriter("dest/BMExports.cs"); - writer.write("// WIP"); + IndentHelper helper = new IndentHelper(writer); + + // write function decls + helper.puts(""); + helper.puts("#region Function Defines"); + helper.puts(""); + + for (ExpFctDecl fctdecl : data) { + // write annotation + // summary (just plain function name) + helper.puts("/// "); + helper.printf("/// %s", fctdecl.mFctName); + helper.puts("/// "); + // parameter list + for (ExpFctParamDecl paramdecl : fctdecl.mFctParams) { + helper.printf("/// Type: %s. %s%s", paramdecl.mVarName, + paramdecl.mVarType.toCType(), (paramdecl.mIsInput ? "" : "This is OUT parameter. "), + paramdecl.mVarDesc); + } + // return value + helper.puts("/// True if no error, otherwise False."); + + // write real declaration + // first, write DllImportAttribute + helper.printf( + "[DllImport(g_DllName, EntryPoint = \"%s\", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi, ExactSpelling = true)]", + fctdecl.mFctName); + // second, write return value MarshalAsAttribute + helper.printf("[return: MarshalAs(UnmanagedType.U1)]"); + // then, before we write function body, we need origanize its parameter list + // first + Vector cs_param_list = new Vector(); + for (ExpFctParamDecl paramdecl : fctdecl.mFctParams) { + // create string builder + StringBuilder sb = new StringBuilder(); + // push in out symbol + if (paramdecl.mIsInput) { + sb.append("[In, "); + } else { + sb.append("[Out, "); + } + // get interop type now + CsInteropType interop_type = getCsInteropType(paramdecl); + // push MarshalAsAttribute + sb.append("MarshalAs("); + sb.append(interop_type.mMarshalAs); + sb.append(") "); + // push out keyword if parameter is out parameter + if (!paramdecl.mIsInput) { + sb.append("out "); + } + // push parameter cs type + sb.append(interop_type.mCsType); + sb.append(" "); + // push parameter name + sb.append(paramdecl.mVarName); + // insert built string into list + cs_param_list.add(sb.toString()); + } + // join built parameter list and output real function declaration + helper.printf("internal static extern bool %s(%s);", fctdecl.mFctName, + cs_param_list.stream().collect(Collectors.joining(", "))); + } + + helper.puts(""); + helper.puts("#endregion"); + helper.puts(""); + writer.close(); } - + } diff --git a/CodeGen/BMapBindings/ExpFctDecl.java b/CodeGen/BMapBindings/ExpFctDecl.java index 9eecefb..6f8256b 100644 --- a/CodeGen/BMapBindings/ExpFctDecl.java +++ b/CodeGen/BMapBindings/ExpFctDecl.java @@ -1,16 +1,29 @@ import java.util.Vector; +/** + * The class represent an export BMap function. + */ public class ExpFctDecl { - + + /** + * The name of this function. + */ public String mFctName; + /** + * The return value type of this function. + */ public VariableType mFctRetType; + /** + * The parameters (arguments) list of this function. Each items are + * {@linkplain ExpFctParamDecl} and represent parameter one by one from left to + * right. + */ public Vector mFctParams; - + public ExpFctDecl() { mFctName = ""; mFctRetType = new VariableType(); mFctParams = new Vector(); } - -} +} diff --git a/CodeGen/BMapBindings/ExpFctParamDecl.java b/CodeGen/BMapBindings/ExpFctParamDecl.java index 6531530..3a868db 100644 --- a/CodeGen/BMapBindings/ExpFctParamDecl.java +++ b/CodeGen/BMapBindings/ExpFctParamDecl.java @@ -1,16 +1,48 @@ +/** + * The class represent a single parameter (argument) of function. This class + * usually is the member of {@linkplain ExpFctDecl}. + */ public class ExpFctParamDecl { - + + /** + * The type of this parameter. + */ public VariableType mVarType; + /** + * The name of this parameter. + */ public String mVarName; + /** + * True if this paramter is marked as input parameter, otherwise false. + *

+ * Input parameter and output paramter is commonly used in C/C++ code. By using + * this feature, each function can receive multiple arguments and return + * multiple arguments without defining a struct to hold it. + *

+ * The type of input parameter is itself. However, the type of output parameter + * is the pointer of itself. So you may need get its pointer type when + * processing output paramter, especially for the scenario that the target + * language do not support explicit output parameter keyword. + */ public boolean mIsInput; + /** + * The description of this parameter. + *

+ * This description is generated by this program. It will indicate the + * underlying C++ type to tell end user how to treat this paramter because some + * target languages' native calling style can not represent these detail. + *

+ * In this program, this field must be written as a annotation of corresponding + * function. + */ public String mVarDesc; - + public ExpFctParamDecl() { mVarType = new VariableType(); mVarName = ""; mVarDesc = ""; mIsInput = true; } - + } diff --git a/CodeGen/BMapBindings/PythonWriter.java b/CodeGen/BMapBindings/PythonWriter.java index 8c0bb6c..121e52c 100644 --- a/CodeGen/BMapBindings/PythonWriter.java +++ b/CodeGen/BMapBindings/PythonWriter.java @@ -39,19 +39,29 @@ public class PythonWriter { return Collections.unmodifiableMap(cache); } - public static String pythonTypeGetter(ExpFctParamDecl paramdecl) { + private static String pythonTypeGetter(ExpFctParamDecl paramdecl) throws IllegalArgumentException { VariableType vt = paramdecl.mVarType; if (!paramdecl.mIsInput) { vt = vt.getPointerOfThis(); } + // create string builder for build final type string StringBuilder sb = new StringBuilder(); + // add type prefix sb.append("bm_"); - sb.append(g_CppTypeMap.get(vt.getBaseType())); + // try getting cpp type from base type + String cpp_type = g_CppTypeMap.get(vt.getBaseType()); + if (cpp_type == null) { + throw new IllegalArgumentException("Unexpected type: " + vt.toCType()); + } + // assign cpp type + sb.append(cpp_type); + // add pointer suffix if (vt.isPointer()) { sb.append("_"); sb.append(String.join("", Collections.nCopies(vt.getPointerLevel(), "p"))); } + // return built type string. return sb.toString(); } @@ -63,11 +73,11 @@ public class PythonWriter { CommonHelper.writeSnippet(writer, "snippets/header.py"); // write function decls - + helper.puts(""); helper.puts("#region Function Defines"); helper.puts(""); - + for (ExpFctDecl fctdecl : data) { // write annotation // function name @@ -90,7 +100,7 @@ public class PythonWriter { helper.puts(""); helper.puts("#endregion"); helper.puts(""); - + writer.close(); } diff --git a/CodeGen/BMapBindings/VariableType.java b/CodeGen/BMapBindings/VariableType.java index 4a50787..c14f031 100644 --- a/CodeGen/BMapBindings/VariableType.java +++ b/CodeGen/BMapBindings/VariableType.java @@ -2,28 +2,51 @@ import java.util.Collections; import java.util.Vector; import java.util.stream.Collectors; +/** + * The class represent the type of each parameters and function return value. + */ public class VariableType { /** - * The base type of this variable which remove all ending stars. Each item is a - * part of namespace string. If no namespace, this Vector will only have one - * item. + * The base type of this variable removing all ending stars (remove all pointer + * levels) Each item in this a part of namespace and the last one must be the + * type itself (without any namespace restriction). If no namespace restriction + * for this type, this Vector will only have one item. + *

+ * For end user, it is enough that knowing the last item is type itself. */ private Vector mBaseType; /** - * The pointer level of this type. It is equal with the count of stars. + * The pointer level of this type. It is equal to the count of trailing star of + * this field in C style representation. */ private int mPointerLevel; + /** + * Construct an empty varible type. This is commonly used constructor. + */ public VariableType() { mBaseType = new Vector(); mPointerLevel = 0; } + /** + * The constructor used for cloning self. This constructor is only can be used + * by self. + * + * @param base_type The hierarchy of the variable type. + * @param pointer_level The pointer level of new created variable type. + */ private VariableType(Vector base_type, int pointer_level) { mBaseType = (Vector) base_type.clone(); mPointerLevel = pointer_level; } + /** + * Set this variable type with a type string in C/C++ style. For example + * "NSTest::NSTest2::MyType**". + * + * @param ctype The type string in C/C++ style. + */ public void fromCType(String ctype) { if (ctype.isEmpty()) throw new IllegalArgumentException("empty string can not be parsed."); @@ -43,7 +66,7 @@ public class VariableType { namepart = ctype.substring(0, star_pos); mPointerLevel = len - star_pos; } - + // resolve name part mBaseType.clear(); for (String item : namepart.split("::")) { @@ -51,31 +74,78 @@ public class VariableType { } } + /** + * Build a type string represented by this variable type in C/C++ style. + * + * @return The type string in C/C++ style. + */ public String toCType() { return mBaseType.stream().collect(Collectors.joining("::")) + String.join("", Collections.nCopies(mPointerLevel, "*")); } + /** + * Get the base type of this variable type without any namespace. It just simply + * get the last entry in type hierarchy. + * + * @return The base type string without namespace prefix. + */ public String getBaseType() { return mBaseType.lastElement(); } + /** + * Check whether this variable type is a pointer. This function just check + * whether the pointer level of this variavle type is zero. + * + * @return True if it is pointer, otherwise false. + */ public boolean isPointer() { return mPointerLevel != 0; } - + + /** + * Return the pointer level of this variable type. You can simply assume the + * pointer level is equal to the count of trailing star. + * + * @return The pointer level integer. Zero means that this type is not a + * pointer. + */ public int getPointerLevel() { return mPointerLevel; } - + + /** + * Return the clone of the type hierarchy of this variable type. + *

+ * It is rarely used. This only should be used when you need the namespace + * hierarchy of this variable type. + * + * @return The clone of current variable type hierarchy. + */ public Vector getBaseTypeHierarchy() { return (Vector) mBaseType.clone(); } + /** + * Check whether this type is a valid one. It actually check whether type + * hierarchy include at least one entry. + * + * @return True if no problem of this type, otherwise false. + */ public boolean isValid() { return mBaseType.size() != 0; } + /** + * Return a new created variable type which is the pointer of this variable + * type. + *

+ * In internal implementation, it just create a clone of current variable type + * with the increase of pointer level by 1. + * + * @return The new created pointer type of this variable type. + */ public VariableType getPointerOfThis() { return new VariableType(mBaseType, mPointerLevel + 1); } diff --git a/README.md b/README.md index cc94e67..5a551f7 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,9 @@ This project welcome everyone's contribution, except the employee of Dassault, w The aim of this project is creating a universal library which can read / write CMO files or any other Virtools files without any Virtools dependencies. This project will not link any original Virtools dynamic library. So this project can be ported to any platform if the compiler supports. This project only involving specific Virtools version, 2.1. Other Virtools versions are not considered by this project. -This project is based on reverse work of CK2.dll, VxMath.dll and CK2_3D.dll. The program [unvirt](https://aluigi.altervista.org/papers.htm#unvirt) created by Luigi Auriemma, which is licensed by GPL-v2, also help my work. +This project is based on reverse work of `CK2.dll`, `VxMath.dll` and `CK2_3D.dll`. The program [unvirt](https://aluigi.altervista.org/papers.htm#unvirt) created by Luigi Auriemma, which is licensed by GPL-v2, also help my work. -**The difference between this project and other Virtools libraries, is that we are not focusing on re-creating the whole Virtools engine. We only focus on the Virtools files RW, and we only just implement a minimalist Virtools environment for achieving this.** +**The difference between this project and other Virtools libraries (e.g. [doyaGu/CK2](https://github.com/doyaGu/CK2)), is that we are not focusing on re-creating the whole Virtools engine. We only focus on the Virtools files RW, and we only just implement a minimalist Virtools environment for achieving this.** ## Goals @@ -66,7 +66,7 @@ These features explicitly will not be merged. This project require: -* The compiler supporting C++20 +* The compiler supporting C++ 20 * Littile-endian architecture system. * zlib * [stb](https://github.com/nothings/stb) (For image read / write)