Skip to content

Commit cb93d13

Browse files
authored
Create splashkit-online-c-sharp-support-research.md
- written by Jessica Balsillie
1 parent 94bd20f commit cb93d13

File tree

1 file changed

+234
-0
lines changed

1 file changed

+234
-0
lines changed
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
---
2+
title: SplashKit Online C# Support Research
3+
description:
4+
A report outlining foundation research into how C# could be supported in SplashKit Online.
5+
---
6+
7+
# SplashKit Online C# Support Research
8+
Finalised 09/10/2024, by Jessica Balsillie
9+
10+
<sup><sub>_Transcribed from PDF to markdown by Sean Boettger - any mistakes in formatting are my fault!_</sup></sub>
11+
12+
## Background: C# on the desktop
13+
14+
There are two kinds of C# desktop applications: bytecode executables and native executables.
15+
16+
A bytecode C# executable is a list of instructions for the C# virtual machine, which can either be interpreted by a VM application or JIT-compiled into native machine code. As such, to run a bytecode C# executable, a separate runtime application, the CLR (common language runtime) must be present on the system to do this processing. This runtime also manages additional things such as garbage collection.
17+
18+
A native C# executable is a normal executable which the processor can run directly, like those of a language like C. The bytecode generated by the C# compiler is further compiled into machine code, and responsibilities that would usually fall to the CLR must be handled by code in the executable itself.
19+
20+
# C# on the web
21+
22+
To use C# on the web, our machine code becomes WASM, and we thus have several options:
23+
24+
1. An interpreter.
25+
2. A compiler producing WASM binaries with a runtime dependency.
26+
3. A compiler producing WASM binaries with an embedded runtime.
27+
4. A C#-to-[language]-to-WASM compiler.
28+
29+
For (1) and (2), the runtime component must run in the browser; i.e. it must be written in JavaScript, have a WASM binary, or be compilable to a WASM binary somehow (which additionally requires that we can license the source code properly.)
30+
31+
For (3) and (4), the solution can either run in the browser, or on a server, with the client receiving a WASM binary. In terms only of this problem, the latter is vastly simpler, as it allows us to run the solution in a normal environment. However, in terms of the problem of SKO broadly, it makes the website substantially more complex and expensive to run, as the site could no longer be statically served.
32+
33+
(Another solution is to forego WASM and use a C#-to-JavaScript transpiler. This would of course be slower, but it may not be so slow as to seriously affect the performance of the site. However, transpilation is complicated by the many differences between the languages, and already existing solutions are slim. Furthermore, if a serverless approach is desired, such a transpiler would also itself need to be made to run in the browser. Ultimately, we likely must compile C# to WASM regardless, and so this is the solution to pursue.)
34+
35+
### wasm-tools
36+
37+
The solution most supported by Microsoft would be to use the dotnet SDK and the wasm-tools workload, which relies ultimately on Emscripten. This will produce a bundle consisting of a WASM application binary (CLR included) and a JavaScript runtime for handling I/O and so on. The SDK would either need be run on a server or be made to run in the browser.
38+
39+
To reproduce a basic C# WASM project requires the following:
40+
1. Install the .NET CLI
41+
2. Run `dotnet workload install wasm-tools`
42+
3. Run `dotnet workload install wasm-experimental`
43+
4. Create the C# project with `dotnet new wasmbrowser`
44+
5. Build the project with `dotnet build`
45+
46+
The directory `./wwwroot` will contain a dummy HTML file and a JavaScript file with basic usage of the .NET JS library, which handles interfacing with the WASM executable. The library itself and all generated files will be located in `./bin/[build type]/[version]/wwwroot`.
47+
48+
### A web compiler
49+
50+
We must not only run C# on the web, but compile C# on the web. Compiling the SDK itself to WASM would be a significant undertaking, and so alternative uses of it are more preferable. We can instead use the SDK to compile a C#-to-bytecode compiler. The C# Roslyn compiler is well-supported as a library ( `Microsoft.CodeAnalysis.CSharp` ), and as binaries generated by the SDK contain a CLR, they can execute arbitrary C# bytecode assemblies.
51+
52+
Usage of the Roslyn compiler library is essentially as follows:
53+
1. Parse source code into an AST with `CSharpSyntaxTree.ParseText`
54+
2. Load relevant assemblies as `MetadataReference` objects.
55+
3. Create a compilation object with `CSharpCompilation`.Create from the AST and metadata references.
56+
4. Emit bytecode with `CSharpCompilation.Emit`
57+
5. Load the bytecode as an assembly with `AppDomain.CurrentDomain.Load`
58+
59+
To load the other assemblies that the user's assembly will depend on, it is necessary to fetch them from the server. Fortunately, the HttpClient class is already implemented by the WASM runtime and makes use of the browser's native facilities for this. For basic functionality, the following assemblies are adequate:
60+
61+
1. `mscorlib.dll`
62+
2. `netstandard.dll`
63+
3. `System.dll`
64+
4. `System.Runtime.dll`
65+
66+
These pre-built .NET assemblies are not necessarily cross-platform, and should be sourced from the C# WASM SDK itself. (I simply copied them from the `./bin` folder of the project and placed them at the root of the server.)
67+
68+
## SplashKit in web C#
69+
70+
Merely being able to compile and run C# code in the browser is not enough for SKO to support C#. We also need for the user's C# assembly to make use of the SplashKit runtime and call SplashKit functions somehow.
71+
72+
### Native dependencies
73+
74+
The C# WASM toolkit provides support for native dependencies; that is, for accessing functions and types in unmanaged, non-C# binaries.
75+
76+
`.csproj`
77+
```C#
78+
<ItemGroup>
79+
<NativeFileReference Include="SplashKitBackendWASM.o" />
80+
</ItemGroup>
81+
```
82+
83+
`SplashKit.cs (generated by the translator)`
84+
```C#
85+
// v modified to ref our library
86+
[DllImport("SplashKitBackendWASM", CallingConvention=CallingConvention.Cdecl, EntryPoint="__sklib__draw_circle__color__double__double__double private static extern void __sklib__draw_circle__color__double__double__double(__sklib_color clr, double x, double y, double radius);
87+
88+
// ...
89+
90+
public static void DrawCircle(Color clr, double x, double y, double radius)
91+
{
92+
// ...
93+
__sklib__draw_circle__color__double__double__double(__skparam__clr, __skparam__x, __skparam__y, __skparam__radius);
94+
}
95+
```
96+
As the SplashKit Online project has already generated a WASM library version of SplashKit, we can use this C# functionality to call SplashKit functions directly from the library. A set of bindings would be needed, as unmanaged code does not expose what functions it contains, but this is already generated by the SplashKit translator project, and so little additional work is necessary.
97+
98+
To use the C# SplashKit bindings, which can be found in `SplashKit.cs` under `generated/csharp` in the `splashkit-core` project, the library must be compiled with the C bindings which can themselves be found under `generated/clib`.
99+
100+
- `lib_type_mapper.cpp`
101+
- `lib_type_mapper.h`
102+
- `sk_clib.cpp`
103+
- `sk_clib.h`
104+
105+
The `SplashKitBackendWASM` file referenced above must be an object file (`.o`) emitted by the Emscripten compiler. It should be compiled with the flag `-fPIC` (Position Independent Code), and linked with the flags `-shared -s RELOCATABLE=1`, in order to create a relocatable library that can be linked against by the C# WASM SDK.
106+
107+
```makefile
108+
emcc $(SRC_FILE) -fPIC -o $(OBJ_FILE)
109+
# ...
110+
emcc $(OBJ_FILES) -shared -s RELOCATABLE=1 -o SplashKitBackendWASM.o
111+
```
112+
113+
(I faced issues with undefined SDL symbols while building, but was unable to determine their cause. When compiling `SplashKitBackendWASM.o`, in which these symbols are referenced, the Emscripten compiler did not complain about them; only the C# SDK identified them. For the sake of prototyping, I used the MSBuild property `<WasmAllowUndefinedSymbols>True</WasmAllowUndefinedSymbols>` to ignore these errors.)
114+
115+
This allowed me to successfully use functions like `WriteLine` from the project's code in the browser.
116+
117+
As the WASM SDK uses Emscripten in order to link this object file, it may be that the Roslyn compiler library simply does not support this native dependencies feature, and so there is no natural way in which to link the SplashKit library to the user's own code. Due to a lack of substantial documentation, I was unable to confirm this.
118+
119+
Regardless, linking the project's own assembly to the user's assembly sufficed for a prototype, and I was able to call SplashKit functions from the user's code. In
120+
future, a dummy assembly that does nothing but link to the SplashKit object file could be generated instead, to avoid bloat and namespace pollution, but that was
121+
unnecessary for this prototype.
122+
123+
(User code)
124+
```C#
125+
using System;
126+
using System.Threading.Tasks;
127+
using SplashKitSDK;
128+
129+
public class Program {
130+
public static async Task<string> Run(){
131+
SplashKitSDK.SplashKit.WriteLine("Hello from SplashKit.");
132+
return "Hello from the user code.";
133+
}
134+
}
135+
```
136+
137+
(Project code)
138+
```C#
139+
var assembly = /* compiled assembly */;
140+
var types = assembly.GetExportedTypes();
141+
var type = types.FirstOrDefault();
142+
var methodInfo = type.GetMethod("Run");
143+
var instance = Activator.CreateInstance(type);
144+
var result = await (Task<string>)methodInfo.Invoke(instance, null);
145+
```
146+
147+
As I was unable to resolve the SDL symbol issues, I was unable to prototype the graphics and audio subsystems of SplashKit. Attempting to use functions like `OpenWindow` would naturally yield runtime errors. It is unclear why these symbols are undefined, as they are usually exported properly when building the SplashKit Online JavaScript runtime. There may be issues in my own development environment.
148+
149+
### Alternative: JavaScript bindings
150+
151+
Rather than interfacing with a library, the JavaScript interop functionality in the C# WASM toolkit could instead be used to call SplashKit functions using the existing JavaScript binding. This interop facility is provided in the namespace `System.Runtime.InteropServices.JavaScript`.
152+
```C#
153+
// function module
154+
[JSImport("write_line", "SplashKitBackendWASM")]
155+
static partial string WriteLine(string data);
156+
```
157+
158+
For simple functions, it is trivial to generate bindings like this, as primitives are automatically marshalled. However, marshalling becomes a problem when more complex types are involved, as JavaScript is of course dynamically-typed.
159+
160+
Marshalling can be specified with attributes.
161+
```C#
162+
[JSImport("getDay", "MyDate")]
163+
public static partial int GetDay([JSMarshalAs<JSType.Date>] DateTime date);
164+
```
165+
166+
```C#
167+
[JSImport("getToday", "MyDate")]
168+
[return: JSMarshalAs<JSType.Date>]
169+
public static partial DateTime GetToday();
170+
```
171+
172+
References to JavaScript objects can be manipulated with the `JSObject` type, however function fields in objects cannot be accessed, and so additional glue code would need be written in JavaScript to facilitate their use.
173+
174+
```JavaScript
175+
// non-glue
176+
class MyClass {
177+
function MyFunction(args...){
178+
// ...
179+
}
180+
}
181+
182+
// glue
183+
function _MyClass_MyFunction(instance, args...){
184+
return instance.MyFunction(args...);
185+
}
186+
```
187+
``` C#
188+
[JSImport("_MyClass_MyFunction")]
189+
public static partial ... MyFunction(JSObject instance, args...);
190+
```
191+
192+
In future, there may be support for serialising and deserialising of objects via JSON, but this is currently not supported. ([This is the relevant GitHub issue.](https://Sorry-the-pdf-I-transcribed-this-from-had-links-removed.../)) As such, glue code is necessary to perform this marshalling explicitly. Furthermore, marshalling through JSON will naturally have an adverse impact on performance.
193+
``` JavaScript
194+
class MyStruct {
195+
constructor(x) {
196+
this.x = x;
197+
}
198+
}
199+
200+
function createStruct(){
201+
return new MyStruct(123);
202+
}
203+
204+
function __createStruct(){
205+
return MyToJsonFunction(createStruct());
206+
}
207+
```
208+
``` C#
209+
public static MyStruct {
210+
public int x;
211+
}
212+
213+
[JSImport("__createStruct")]
214+
public static partial string __CreateStruct();
215+
216+
public static MyStruct CreateStruct(){
217+
return MyFromJsonFunction(__CreateStruct());
218+
}
219+
```
220+
221+
Given these marshalling issues: even with the SplashKit JavaScript binding already existing, accessing it from C# would require writing yet more bindings for the entirety of SplashKit, which was not within the scope of this research.
222+
223+
## Conclusion
224+
225+
Not only is it possible to compile C# as a WASM executable, it is possible to embed a C# compiler in the browser. Linking the SplashKit library to this executable, and to assemblies generated by the in-browser compiler proved possible, but faced several major issues. SDL libraries were unable to be linked properly for unknown reasons, and so the majority of user-facing functionality remains unimplemented. Further research is required in order to support C# in SplashKit Online, for which this work provides a foundation.
226+
227+
## References
228+
229+
1. [C# WASM example project guide](https://Sorry-the-pdf-I-transcribed-this-from-had-links-removed.../)
230+
2. [C# WASM example project](https://Sorry-the-pdf-I-transcribed-this-from-had-links-removed.../)
231+
3. [Documentation for Microsoft.CodeAnalysis.CSharp](https://Sorry-the-pdf-I-transcribed-this-from-had-links-removed.../)
232+
4. [Another web-based C# compiler project](https://Sorry-the-pdf-I-transcribed-this-from-had-links-removed.../)
233+
5. [C# WASM native dependencies](https://Sorry-the-pdf-I-transcribed-this-from-had-links-removed.../)
234+
6. [.NET WASM Javascript interop guide](https://Sorry-the-pdf-I-transcribed-this-from-had-links-removed.../)

0 commit comments

Comments
 (0)