chrisdill / raylib-cs

C# bindings for raylib, a simple and easy-to-use library to learn videogames programming
http://www.raylib.com/
zlib License
851 stars 74 forks source link

Native AOT and link Raylib statically #258

Closed lukaspirkl closed 4 months ago

lukaspirkl commented 4 months ago

Issue description

When C# is used with the raylib the resulting binaries are huge. Usually, there is a whole runtime packed and a lot of DLLs are just laying around. It is possible to publish the project as a single file, but it is not really a "single" as the raylib.dll is still laying next to it. And the executable is still huge. With the latest dotnet features like native ahead-of-time compilation, it could/should be possible to generate just one small executable like when C/C++ is used.

Current state

Right now, with the latest raylib-cs library, it is possible to add <PublishAot>true</PublishAot> into the csproj and it will create native executable (~2MB) and raylib.dll. But it would be nice if the DLL would be linked statically and the whole program would be just a single executable. It would also trim the unused functions from the raylib library so the result would be even smaller.

Experiment

So I tried it. 😊 To minimize all side effects, I started from scratch without any library whatsoever. This is what I did:

And to my surprise, everything was working. Inside the output directory, there were raylib.dll (1.8 MB), Raylib-AOT.exe (1.5 MB), and debug symbols. I continued and added the following text into the csproj as described in the documentation:

<ItemGroup>
  <DirectPInvoke Include="raylib" />
  <NativeLibrary Include="raylib.lib" />
</ItemGroup>

This time the dotnet publish failed.

c:\Dev\Raylib_AOT>dotnet publish
  Determining projects to restore...
  All projects are up-to-date for restore.
  Raylib_AOT -> c:\Dev\Raylib_AOT\bin\Release\net8.0\win-x64\Raylib_AOT.dll
LINK : warning LNK4098: defaultlib 'MSVCRT' conflicts with use of other libs; use /NODEFAULTLIB:library [c:\Dev\Raylib_AOT\Raylib_AOT.csproj]
raylib.lib(rcore.obj) : error LNK2019: unresolved external symbol timeBeginPeriod referenced in function InitPlatform [c:\Dev\Raylib_AOT\Raylib_AOT.csproj]
raylib.lib(rcore.obj) : error LNK2019: unresolved external symbol timeEndPeriod referenced in function ClosePlatform [c:\Dev\Raylib_AOT\Raylib_AOT.csproj]
raylib.lib(rglfw.obj) : error LNK2019: unresolved external symbol __imp_ChoosePixelFormat referenced in function _glfwInitWGL [c:\Dev\Raylib_AOT\Raylib_AOT.csproj]
raylib.lib(rglfw.obj) : error LNK2019: unresolved external symbol __imp_CreateBitmap referenced in function createIcon [c:\Dev\Raylib_AOT\Raylib_AOT.csproj]
raylib.lib(rglfw.obj) : error LNK2019: unresolved external symbol __imp_CreateDCW referenced in function _glfwGetGammaRampWin32 [c:\Dev\Raylib_AOT\Raylib_AOT.csproj]
raylib.lib(rglfw.obj) : error LNK2019: unresolved external symbol __imp_CreateRectRgn referenced in function updateFramebufferTransparency [c:\Dev\Raylib_AOT\Raylib_AOT.csproj]
raylib.lib(rglfw.obj) : error LNK2019: unresolved external symbol __imp_DeleteDC referenced in function _glfwGetGammaRampWin32 [c:\Dev\Raylib_AOT\Raylib_AOT.csproj]
raylib.lib(rglfw.obj) : error LNK2019: unresolved external symbol __imp_DeleteObject referenced in function createIcon [c:\Dev\Raylib_AOT\Raylib_AOT.csproj]
raylib.lib(rglfw.obj) : error LNK2019: unresolved external symbol __imp_DescribePixelFormat referenced in function _glfwCreateContextWGL [c:\Dev\Raylib_AOT\Raylib_AOT.csproj]
raylib.lib(rglfw.obj) : error LNK2019: unresolved external symbol __imp_GetDeviceCaps referenced in function _glfwGetHMONITORContentScaleWin32 [c:\Dev\Raylib_AOT\Raylib_AOT.csproj]
raylib.lib(rglfw.obj) : error LNK2019: unresolved external symbol __imp_SetPixelFormat referenced in function _glfwCreateContextWGL [c:\Dev\Raylib_AOT\Raylib_AOT.csproj]
raylib.lib(rglfw.obj) : error LNK2019: unresolved external symbol __imp_CreateDIBSection referenced in function createIcon [c:\Dev\Raylib_AOT\Raylib_AOT.csproj]
raylib.lib(rglfw.obj) : error LNK2019: unresolved external symbol __imp_GetDeviceGammaRamp referenced in function _glfwGetGammaRampWin32 [c:\Dev\Raylib_AOT\Raylib_AOT.csproj]
raylib.lib(rglfw.obj) : error LNK2019: unresolved external symbol __imp_SetDeviceGammaRamp referenced in function _glfwSetGammaRampWin32 [c:\Dev\Raylib_AOT\Raylib_AOT.csproj]
raylib.lib(rglfw.obj) : error LNK2019: unresolved external symbol __imp_SwapBuffers referenced in function swapBuffersWGL [c:\Dev\Raylib_AOT\Raylib_AOT.csproj]
raylib.lib(rglfw.obj) : error LNK2019: unresolved external symbol __imp_DragQueryFileW referenced in function windowProc [c:\Dev\Raylib_AOT\Raylib_AOT.csproj]
raylib.lib(rglfw.obj) : error LNK2019: unresolved external symbol __imp_DragQueryPoint referenced in function windowProc [c:\Dev\Raylib_AOT\Raylib_AOT.csproj]
raylib.lib(rglfw.obj) : error LNK2019: unresolved external symbol __imp_DragFinish referenced in function windowProc [c:\Dev\Raylib_AOT\Raylib_AOT.csproj]
raylib.lib(rglfw.obj) : error LNK2019: unresolved external symbol __imp_DragAcceptFiles referenced in function createNativeWindow [c:\Dev\Raylib_AOT\Raylib_AOT.csproj]
bin\Release\net8.0\win-x64\native\Raylib_AOT.exe : fatal error LNK1120: 19 unresolved externals [c:\Dev\Raylib_AOT\Raylib_AOT.csproj]
C:\Users\pirkl\.nuget\packages\microsoft.dotnet.ilcompiler\8.0.7\build\Microsoft.NETCore.Native.targets(370,5): error MSB3073: The command ""C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.40.33807\bin\Hostx64\x64\link.exe" @"obj\Release\net8.0
\win-x64\native\link.rsp"" exited with code 1120. [c:\Dev\Raylib_AOT\Raylib_AOT.csproj]

I tried some random things as I had no idea what I am doing. I ended up with the following text in the csproj:

<ItemGroup>
  <DirectPInvoke Include="raylib" />
  <NativeLibrary Include="raylib.lib" />
  <NativeLibrary Include="c:\Program Files (x86)\Windows Kits\10\Lib\10.0.22621.0\um\x64\Gdi32.lib" />
  <NativeLibrary Include="c:\Program Files (x86)\Windows Kits\10\Lib\10.0.22621.0\um\x64\shell32.lib" />
</ItemGroup>

And most of the errors disappeared:

c:\Dev\Raylib_AOT>dotnet publish
  Determining projects to restore...
  All projects are up-to-date for restore.
  Raylib_AOT -> c:\Dev\Raylib_AOT\bin\Release\net8.0\win-x64\Raylib_AOT.dll
  Generating native code
LINK : warning LNK4098: defaultlib 'MSVCRT' conflicts with use of other libs; use /NODEFAULTLIB:library [c:\Dev\Raylib_AOT\Raylib_AOT.csproj]
raylib.lib(rcore.obj) : error LNK2019: unresolved external symbol timeBeginPeriod referenced in function InitPlatform [c:\Dev\Raylib_AOT\Raylib_AOT.csproj]
raylib.lib(rcore.obj) : error LNK2019: unresolved external symbol timeEndPeriod referenced in function ClosePlatform [c:\Dev\Raylib_AOT\Raylib_AOT.csproj]
bin\Release\net8.0\win-x64\native\Raylib_AOT.exe : fatal error LNK1120: 2 unresolved externals [c:\Dev\Raylib_AOT\Raylib_AOT.csproj]
C:\Users\pirkl\.nuget\packages\microsoft.dotnet.ilcompiler\8.0.7\build\Microsoft.NETCore.Native.targets(370,5): error MSB3073: The command ""C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.40.33807\bin\Hostx64\x64\link.exe" @"obj\Release\net8.0
\win-x64\native\link.rsp"" exited with code 1120. [c:\Dev\Raylib_AOT\Raylib_AOT.csproj]

Not great, not terrible. But my gut feeling is telling me that I am on the wrong way and there should be some other solution than adding paths to random system libraries.

The minimal project: Raylib_AOT.zip

BTW, I tried the same approach with Lua library and it worked without any error or warning. Dotnet executable compiled to native code with lua linked statically just worked.

nickyMcDonald commented 4 months ago

Both timeBeginPeriod and timeEndPeriod use Winmm.lib. My path to these are "C:\Program Files (x86)\Windows Kits\10\Lib\10.0.22621.0\um\x64\WinMM.Lib". Not sure if this would help but it looks like Visual Studio uses macros for paths to the Windows SDK. Looking at the path for the DLLs you are importing $(WindowsSDK_LibraryPath_x64) seems like its the right one.

droyer57 commented 4 months ago

I tried it on Linux and it works.

<ItemGroup>
    <DirectPInvoke Include="raylib" Visible="false" />
    <NativeLibrary Include="libraylib.a" />
</ItemGroup>

I didn't know that it was possible to link statically with native AOT. That's great, thank you.

I haven't tried it on Windows.

lukaspirkl commented 4 months ago

@nickyMcDonald Thank you! It is working now. And it doesn't look that terrible with the $(WindowsSDK_LibraryPath_x64). I found out that even this is not necessary and using just the file name is enough.

So for Windows, the complete ItemGroup for static linking looks like this:

<ItemGroup>
  <DirectPInvoke Include="raylib" />
  <NativeLibrary Include="raylib.lib" />
  <NativeLibrary Include="Gdi32.lib" />
  <NativeLibrary Include="shell32.lib" />
  <NativeLibrary Include="WinMM.Lib" />
</ItemGroup>

The dotnet publish is now returning only the warning and it is producing a nice single executable with everything inside.

c:\Dev\Raylib_AOT>dotnet publish
  Determining projects to restore...
  Restored c:\Dev\Raylib_AOT\Raylib_AOT.csproj (in 215 ms).
  Raylib_AOT -> c:\Dev\Raylib_AOT\bin\Release\net8.0\win-x64\Raylib_AOT.dll
  Generating native code
LINK : warning LNK4098: defaultlib 'MSVCRT' conflicts with use of other libs; use /NODEFAULTLIB:library [c:\Dev\Raylib_
AOT\Raylib_AOT.csproj]
  Raylib_AOT -> c:\Dev\Raylib_AOT\bin\Release\net8.0\win-x64\publish\

I am still curious about the warning message. I found these two pieces of information:

Using <LinkerArg Include="/VERBOSE:LIB" /> writes to output a lot of lib files that it is searching for. And there is indeed LIBCMT.lib together with MSVCRT.lib. When I include <LinkerArg Include="/NODEFAULTLIB:msvcrt.lib" />, the warning disappears. However, as mentioned in the StackOverflow answer, this doesn't actually fix the problem. If I understand it correctly, Raylib and AOT are using different C++ runtime libraries. I'm looking for information on how to switch it, but I would appreciate some help or direction as this is quite new to me.

nickyMcDonald commented 4 months ago

Ok.. so for as far as I know the nativeaot /MD, /MT, /LD (Use Run-Time Library) compiler option is not accessable. If they were accessable then the correct compiler option would be /MD I think since the release version of raylib is built with that option.

Fortunatly a workaround is to just build raylib itself with the compiler option /MT set. That seemed to work for me because the dotnet publish warning dissapeared.

To build it I used the raylib solution file provided in the source code file: "/projects/VS2022/raylib.sln"

Then for "Release" you want to set the runtime library to /MT not /MD: Screenshot 2024-07-28 142005

Im not sure if it was neccisary to build the dll but I built both the dll and lib versions of release: Screenshot 2024-07-28 142703

The dll build location is "/projects/VS2022/build/raylib/bin/x64/Release.DLL" The lib build location is "/projects/VS2022/build/raylib/bin/x64/Release"

Btw when building raylib with /MT set there probably will be a lot of linker errors such as: LINK : warning LNK4098: defaultlib 'LIBCMT' conflicts with use of other libs; use /NODEFAULTLIB:library Those are fine since they are just the example projects which are set with /MD and not the raylib.dll itself.

lukaspirkl commented 4 months ago

@nickyMcDonald Thank you so much! I was so focused on CMake and console when building C++ stuff that I forgot I could build Raylib directly in Visual Studio. :laughing: Everything is working flawlessly without any warning.