Syvempi katsaus .NET-ohjelmien kääntämiseen
Tutustutaan seuraavaksi vähän tarkemmin C#-kääntäjän toimintaan sekä .NET-ajoympäristöön. Kuten luvussa Lähdekoodista prosessorille avattiin, C#-kääntäjä tuottaa oletuksena käyttöjärjestelmäriippumattomalla välikielellä kirjoitettua koodia. Tässä luvussa tutkitaan hieman tarkemmin dotnet
-työkalun tuottamat tiedostot ymmärtääkseen, kuinka käyttöjärjestelmäriippumaton ohjelmatiedosto muuttuu ajon aikana käyttöjärjestelmä- ja prosessoririippuvaiseksi konekieleksi.
Luku syventyy .NET-ajoympäristön toimintaan ja paikoin ylittää Ohjelmointi 1 -kurssin tietovaatimukset. Luvussa oleviin tietoihin voi siis tutustua oman halun mukaan.
1. Projektin luominen
Tehdään aluksi uusi puhdas Jypeli-projekti dotnet
-työkalulla. dotnet
on tällä hetkellä käytössä olevan .NET-ajoympäristön komentorivityökalu, jolla C#-projektit voi luoda, kääntää ja ajaa.
Varmistetaan, että Jypeli-projektin pohja on asennettu
Tämä komento asentaa Jypeli.Templates-pakkauksen virallisesta NuGet.org-pakkausvarastosta.
Kaikki käytössä olevat projektipohjat voi listata seuraavalla komennolla:
Tämä listaa muun muassa juuri asennetut Jypeli-projektipohjat:
Templates Short Name Language Tags -------------------------------------------- ------------------- ---------- ------ ConsoleMain ConsoleMain [C#] Jypeli Jypeli Betaversio FysiikkapeliBeta [C#] Jypeli Fysiikkapeli Fysiikkapeli [C#] Jypeli Peruspeli Peruspeli [C#] Jypeli Tasohyppelypeli Tasohyppelypeli [C#] Jypeli
Luodaan uusi kansio ja siirrytään siihen
mkdir
jacd
-komennoillaLuodaan uusi projekti
Fysiikkapeli
-pohjastaTämä luo uuden
Lumiukko
-nimisen kansion, johon laitetaan C#-projektitiedostot.Avaa
Lumiukko.cs
tekstieditorissa ja lisää siihen yksinkertainen pelikoodiusing Jypeli; public class Lumiukko : PhysicsGame { public void LisaaPalloja() { for (int i = 0; i < 200; i++) { double d = RandomGen.NextDouble(5, 20); PhysicsObject pallo = new PhysicsObject(d, d, Shape.Circle); pallo.Position = RandomGen.NextVector(Level.BoundingRect); pallo.Color = RandomGen.NextColor(); Add(pallo); } } public override void Begin() { GameObject p1 = new GameObject(10, 10, Shape.Circle); Add(p1); LisaaPalloja(); Camera.ZoomToLevel(); } }
Seuraavissa alaluvuissa tutustutaan .NET-ajoympäristöön tämän projektin kautta. Kaikki ajettavat komennot suoritetaan alkaen projektikansiosta.
2. C#-projektin rakenne
Jypelin-projektin luomisen jälkeen Lumiukko
-kansiosta löytyvät seuraavat tiedostot:
.
├── Lumiukko.cs
├── Lumiukko.csproj
└── Ohjelma.cs
Yleisesti ottaen C#-projektit sisältävät kolmea eri tiedostotyyppiä.
2.1 Lähdekooditiedostot
Lähdekooditiedostot ovat .cs
-päätteiset tiedostot, kuten aiemmissa luvuissa on pohjustettu. Oletuksena kaikki kansiossa olevat tiedostot sisällytetään kääntämiseen.
Lähdekooditiedostot voi myös jakaa alikansioihin ja tällä tavoin järjestää ohjelma järkeviin kokonaisuuksiin. Jypeli-projekteissa tätä tarvitaan harvemmin, mutta isoimmissa projekteista tästä on valtavasti hyötyä.
2.2 Projektin asetukset
.csproj
-päätteinen tiedosto sisältää asetukset, jolla projekti käännetään. dotnet
-työkalu käyttää tämän tiedoston tiedot muodostaakseen automaattisesti oikeita komentoja csc
-kääntäjälle ja NuGet-pakkauksenhallintatyökalulle. Lisäksi asetustiedostoon voi määritellä erilaisia toimintoja, joita suoritetaan ennen kääntämistä tai sen jälkeen.
.csproj
-tiedosto on tekstitiedosto, joka koostuu XML-elementeista. XML-elementti on tekstipari <T>...</T>
. Elementti alkaa yleensä alkutagilla <T>
ja päättyy lopetustagilla </T>
. Tagien välissä oleva sisältö ...
on elementin sisältö.
Jotkut elementit ovat muodossa <T/>
, joilla ei ole sisältöä eikä lopetustagia.
Esimerkiksi Jypeli-projetkin projetkitiedosto näyttää seuraavalta:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<PublishReadyToRun>false</PublishReadyToRun>
<TieredCompilation>false</TieredCompilation>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Jypeli.NET" Version="10.*" />
<PackageReference Include="Jypeli.FarseerPhysics.NET" Version="1.0.*" />
</ItemGroup>
</Project>
Tutustutaan joihinkin .csproj
-tiedostossa oleviin asetuksiin. Kaikkien elementtien tarkka dokumentaatio löytyy .NET SDK -dokumentaatiosta.
2.2.1 TargetFramework
: käytettävä .NET-ajoympäristö ja -rajapintaversio
C#-ohjelmat voi kääntää ja ajaa erilaisissa ajoympäristöissä. Tällä hetkellä seuraavat ajoympäristöt ja rajapintaversiot ovat olemassa ja tuetut C#-kielessä:
- .NET Framework ja Mono: .NET Framework on alkuperäinen Microsoftin kehittämä ajoympäristö Windows-käyttöjärestelmille (julkaistu 2000). Mono on puolestaan .NET Frameworkin kanssa yhteensopiva, Windows- ja Unix-käyttöjärjestelmille kehitetty avoimen lähdekoodin .NET-ajoympäristö (julkaistu 2004).
.csproj
-tiedostossa sen tunnus onnet20
,net35
, jne. - .NET Core on Microsoftin tukema avoimen lähdekoodin toteutus .NET-ajoympäristöstä (julkaistu 2016). Samoin kuin Mono, .NET Core on tarkoitettu Windows- sekä Unix-järjestelmille.
.csproj
-tiedostossa sen tunnus onnetcoreapp1.0
,netcoreapp2.0
, jne. - .NET 5 on .NET Core -ajomypäristön tämänhetkinen versio. Alkaen tästä versiosta, .NET Core -ajoympäristö nimetään ".NET-ajoympäristöksi".
.csproj
-tiedostossa sen tunnus onnet5.0
- .NET Standard on määritelmä rajapinnoista (esim.
System.Console
,System.Text.StringBuilder
), joita ajoympäristö toteuttaa. C#-koodi, joka on käännetty käyttäen .NET Standard -rajapintakokoelmaa voidaan ajaa ajoympäristöissä, jotka tukevat tämän kokoelman. Esimerkiksi .NET Standard 2.0 -rajapintakokoelmalle käännettyä C#-koodia voi ajaa NET Framework 4.6, .NET Core 3.0 ja Mono 5.4 -ympäristöissä ilman yhteensopivuusongelmia..csproj
-tiedostossa sen tunnus onnetstandard1.0
,netstandard1.1
, jne.
On olemassa myös joitakin erikoiskäyttöön tarkoitettuja ajoympäristöjä. Kaikki eri toimivat ajoympäristöt ovat listattu Microsoft Docs -sivustolla.
2.2.2 PackageReference
: ulkoisen kirjaston käyttäminen projektissa
Esimerkiksi
määriittää, että
- Projekti tarvitsee Jypeli.NET -nimisen pakkauksen NuGet-pakkauksenhallintajärjestelmästä
- Projekti tarvitsee pakkauksen uusimman saatavilla olevan 10.x.x version
NuGet on dotnet
-työkalun virallinen pakkauksenhallintatyökalu. Sen avulla .NET-kirjastot voidaan pakata ja julkaista verkkoon muiden kehittäjien käyttöön. Oletuksena dotnet
-työkalu hakee pakkaukset NuGet.org-pakkausvarastosta.
2.3 Resurssit
Resurssitiedostot ovat erilaiset kuvat, tekstit ja äänet, jotka liitetään osaksi projektia. Näiden liittämiseksi on olemassa omat XML-elementit .csproj
-tiedostoon.
3. C#-projektin kääntäminen dotnet
-työkalulla
Tarkastellaan, miten projektin kääntäminen tapahtuu.
Projektin kääntäminen tehdään dotnet build
-komennolla. Tämä käynnistää MSBuild-rakennusohjelman, joka tulkitsee .csproj
sisällön ja ajaa tarvittavat komentoriviohjelmat.
Ajetaan dotnet build
-komento, jolloin saadaan seuraava tuloste:
Microsoft (R) Build Engine version 16.9.0+57a23d249 for .NET
Copyright (C) Microsoft Corporation. All rights reserved.
Determining projects to restore...
Restored XXX\Lumiukko\Lumiukko.csproj (in 4,88 sec).
Lumiukko -> XXX\Lumiukko\bin\Debug\net5.0\Lumiukko.dll
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:07.14
Kääntämisen olennaiset vaiheet:
.csproj
-tiedoston tulkintaMSBuild lukee
.csproj
-tiedoston sekä tarvittavat lisätiedostot, jos niitä on olemassa. Tässä tapauksessa tällaisia lisätiedostoja ei ole, joten tämä vaihe on varsin nopea.NuGet-pakkausten lataaminen ja kerääminen
Kohdassa
MSBuild käyttää NuGet-työkalua ladatakseen kaikki
PackageReference
-pakkaukset ja niiden riippuvuudet.C#-tiedostojen kääntäminen
Kohdassa
MSBuild kutsuu
csc
-kääntäjän antaen sille parametreina NuGet-pakkauksien sisällä olevat kirjastot sekäTargetFramework
-arvosta riippuvat rajapintakirjastot.Huomaa, että kääntäjä tuotti nimenomaan
.dll
-tiedoston, vaikka.csproj
-asetuksissa projekti on ajettava ohjelma.
Tähän palataan myöhemmin.Viimeistely
Lopuksi MSBuild ajaa viimeistelytoimia riippuen
.csproj
-tiedostosta olevista asetuksista. MSBuild muun muassa kopioi ajoon tarvittavat kirjastotLumiukko\bin\Debug\net5.0
-kansioon sekä muodostaa ajoa varten tarvittavat asetukset.
Kääntämisen lopuksi projektikansioon tulee kaksi kansiota lisää: obj
ja bin
. obj
-kansio sisältää kääntämistä nopeuttavia väliaikaisia tiedostoja eikä siis ole kovin mielenkiintoinen. Sen sijaan bin
sisältää lopullisen käännetyn ohjelman ja tarvittavat riippuvuudet. Tutkitaan seuraavaksi bin
-kansion sisältö.
4. Käännetyn .NET-ohjelman rakenne
Tutkitaan edellisessä luvussa käännettyä Jypeli-kirjastolla tehtyä peliä. Käännetty peli löytyy kansiosta bin\Debug\net8.0
(.NET-version numero saattaa vaihdella). Kansion rakenne on seuraava:
.
├── Cyotek.Drawing.BitmapFont.dll
├── FontStashSharp.dll
├── Jypeli.FarseerPhysics.dll
├── Jypeli.dll
├── Lumiukko.deps.json
├── Lumiukko.dll
├── Lumiukko.exe
├── Lumiukko.pdb
├── Lumiukko.runtimeconfig.dev.json
├── Lumiukko.runtimeconfig.json
├── (lisää dll-tiedostoja)...
├── ref
│ └── Lumiukko.dll
└── runtimes
├── linux-x64
├── osx-arm64
├── osx-x64
├── win
├── win-x64
└── win-x86
4.1 Ohjelmakansion tiedostot ja alikansiot
Kansio sisältää siis kaiken tarvittavan ohjelman ajamiseksi. Käydään läpi ensin "itsestäänselvät" tiedostot ja kansiot läpi.
Tiedostot
Cyotek.Drawing.BitmapFont.dll
FontStashSharp.dll
Jypeli.FarseerPhysics.dll
Jypeli.dll
Microsoft.Win32.SystemEvents.dll
StbImageSharp.dll
StbTrueTypeSharp.dll
System.Drawing.Common.dll
Silk.NET
-alkuisia tiedostoja
ovat .NET-kirjastot, joita peli käyttää. Nämä tiedostot sisältävät järjestelmäriippumatonta ohjelmakoodia Jypelin toimintaa varten. Tiedostot ovat tulevat alun perin Jypeli.NET
- ja Jypeli.FarseerPhysics.NET
-pakkauksista (ja niiden riippuvuuksista), jotka määriteltiin .csproj
-tiedostosta. Lisäksi Silk.NET-alkuiset dll-tiedostot tarjoavat käyttöliittymiä erilaisiin grafiikka- ja multimediateknologioihin, kuten OpenGL:ään ja Vulkaniin, joita peli saattaa hyödyntää grafiikan renderöimiseksi. Yhdessä nämä tiedostot mahdollistavat varsinaisen pelin ajamisen.
runtimes
-kansio sisältää lisäksi käyttöjärjestelmästä riippuvia kirjastoja. Jypelin tapauksessa nämä ovat SDL-grafiikkakirjasto sekä OpenAL-äänikirjasto. Kansion sisältä löytyvät nämä kaksi kirjastoa käännettynä valmiiksi eri käyttöjärjestelmille. Nämäkin tiedostot tulevat NuGet-pakkauksista.
ref
-kansio sisältää niin sanottuja viitekirjastoja, joita ensisijaisesti käytetään kääntämisprosessin nopeuttamiseksi. Viitekirjastoja voi käyttää myös oman projektin rajapinnan jakamiseksi muille ilman sitä, että jaettaisiin samalla itse ohjelmakoodia. Ajettavien ohjelmien tapauksessa tämän kansion sisältön on usein turha.
Loput tiedostot ovat Lumiukko
-alkuiset. Nämä tiedostot liittyvät suoraan käännettyyn projektiin.
4.1.1 deps.json
: lista ajonaikaisista riippuvuuksista
Lumiukko.deps.json
sisältää .NET-ajoympäristölle tarkoitettua tietoa seuraavista asioista:
- Mitä ajoympäristöä ohjelma voi käyttää
- Millä asetuksilla ohjelma käännettiin
- Mitä kirjastoja ohjelman ajamiseen tarvitaan
- Mistä NuGet-pakkauksista kirjastot voi löytää ja miten niiden eheys tarkistetaan
.NET-ajoympäristö hyödyntää tätä tietoa kirjastojen lataamisessa, sillä oletuksena turvallisuussyistä ajoympäristöön pyritään lataamaan vain tarvittavat kirjastot. Tiedostoa käytetään myös siinä tapauksessa, jos kirjastot puuttuvat, jolloin ne voi ladata suoraan verkosta.
4.1.2 runtimeconfig.json
ja runtimeconfig.dev.json
: ajonaikaiset asetukset
Lumiukko.runtimeconfig.json
sisältää tietoa, jota tarvitaan ohjelman käynnistämiseen. Tiedostosta löytyy muun muassa seuraavaa tietoa:
- Mikä ajoympäristö tulee käyttää ohjelman käynnistämiseen
- Miten ajoympäristö tulee käynnistää (esim. miten välikieli käännetään ohjelmakoodiksi)
- Mistä poluista lisäkirjastot tulee etsiä
Toisin kuin Lumiukko.deps.json
, ilman tätä asetustiedostoa .NET-ohjelma ei käynnisty.
Lisätietoja deps.json
ja runtimeconfig.json
-tiedostojen rakenteesta ja tarkoituksesta löytyy .NET-spesifikaatiosta.
4.1.3 .dll
: käännetty C#-ohjelma
Lumiukko.dll
sisältää itse käännetyn C#-ohjelman. C# on käännetty käyttöjärjestelmästä riippumattomaksi CIL-välikieleksi (Common Intermediate Language), jota .NET-ympäristö puolestaan tulkitsee ja kääntää ajon aikana lopulliseksi ajettavaksi koodiksi.
Ohjelman käynnistäminen onnistuu komennolla
dotnet Lumiukko.dll
Tällä komennolla tapahtuu seuraavaa:
dotnet
-työkalu käynnistää tyhjän .NET-ajoympäristön- .NET-ajoympäristö lukee
runtimeconfig.json
jadeps.json
-tiedostot ja konfiguroi itseään tietojen perusteella - .NET-ajoympäristö lukee
Lumiukko.dll
-tiedoston, etsii sieltä aloitusaliohjelmanMain()
ja suorittaa sen - Ajon aikana ajoympäristö saattaa ladata lisäkirjastoja käyttämällä
runtimeconfig.json
jadeps.json
-tiedostossa olevia asetuksia.
4.1.4 .exe
: .NET-ympäristön automaattinen käynnistysohjelma
Vaikka Lumiukko.dll
sisältää itse ohjelmakoodin, ohjelman jatkuva käynnistäminen komentoriviltä voi olla hieman ärsyttävää. Tätä varten MSBuild laittoi kansioon myös klikattavan apuohjelman Lumiukko.exe
. Apuohjelma ei sisällä yhtään projektin omaa ohjelmakoodia, vaan se yksinkertaisesti tekee saman asian kuin dotnet Lumiukko.dll
.
Toisin sanoin Lumiukko.exe
on apuohjelma, joka käynnistää .NET-ajoympäristön ja ajaa Lumiukko.dll
-tiedoston. Apuohjelma on käyttöjärjestelmäriippuvainen, mutta se on sama kaikille .NET 5 -projekteille.
4.2 Käännetyn ohjelmatiedoston sisältö
Tutkitaan lopuksi hieman käännettyä Lumiukko.dll
-tiedostoa. Kuten aiemmin on mainittu, tiedosto sisältää CIL-välikieleksi käännettyä koodia. Varmistetaan, että tämä pitää paikkaansa ja katsotaan hieman, miltä tämä CIL-välikieli näyttää.
Tätä varten tarvitaan jokin takaisinmallinnusohjelma, joka osaa lukea .NET-ajoympäristölle tarkoitetut .dll
-tiedostot. Näistä yleisessä käytössä ovat ILSpy ja dnSpy. Tässä alaluvussa käytetään ILSpy-ohjelmaa, sillä sen tuloste on hieman parempi kuin dnSpy:n.
Ladataan Lumiukko.dll
-tiedosto ILSpy-ohjelmaan (File \(\rightarrow\) Open \(\rightarrow\) Etsitään ja valitaan Lumiukko.dll
).
Ohjelman Assemblies
-ikkunaan avautuu Lumiukko.dll
. Sen sisältöä voi avata / -painikkeilla. Avataan tiedoston sisältö sen verran, että nähdään Lumiukko
-luokka ja sen sisällä olevat aliohjelmat:
Huomataan siis, että ILSpy näkee samat aliohjelmat kuin mitä on määritelty luvun alussa.
Kun klikataan Begin()
-aliohjelma, avautuu viereiseen ikkunaan Begin()
-aliohjelma, jonka ILSpy käänsi CIL-välikielestä takaisin C#-kieleksi.
ILSpy on takaisinmallinnusohjelma, joka siis osaa lukea peliohjelmatiedoston sisällön ja kääntää se takaisin luettavaksi C#-ohjelmaksi. Huomaa, että CIL-välikielestä käännetty C# ei vastaa täysin alkuperäistä koodia. Esimerkiksi Begin
-aliohjelman ensimmäiset kaksi riviä
GameObject p1 = new GameObject(10, 10, Shape.Circle);
Add(p1);
ovat liitetty yhteen, ILSpy-ohjelmassa muotoon
Add(new GameObject(10.0, 10.0, Shape.Circle));
sillä ILSpy yrittä "arvata" alkuperäisen ohjelman rakenteen.
Tutkitaan seuraavaksi, miltä itse CIL-välikieli näyttää. Tätä varten yläpalkin alasvetolaatikosta vaihdetaan C#
-valinta IL with C#
-valinnaksi:
Tämä näyttää seuraavan koodin:
.method public hidebysig virtual
instance void Begin () cil managed
{
// Method begins at RVA 0x20d8
// Code size 58 (0x3a)
.maxstack 3
.locals init (
[0] class [Jypeli]Jypeli.GameObject p1
)
// {
IL_0000: nop
// Add(new GameObject(10.0, 10.0, Shape.Circle));
IL_0001: ldc.r8 10
IL_000a: ldc.r8 10
IL_0013: ldsfld class [Jypeli]Jypeli.Ellipse [Jypeli]Jypeli.Shape::Circle
IL_0018: newobj instance void [Jypeli]Jypeli.GameObject::.ctor(float64, float64, class [Jypeli]Jypeli.Shape)
IL_001d: stloc.0
IL_001e: ldarg.0
IL_001f: ldloc.0
// (no C# code)
IL_0020: call instance void [Jypeli]Jypeli.Game::Add(class [Jypeli]Jypeli.IGameObject)
// LisaaPalloja();
IL_0025: nop
IL_0026: ldarg.0
IL_0027: call instance void Lumiukko::LisaaPalloja()
// base.Camera.ZoomToLevel();
IL_002c: nop
IL_002d: ldarg.0
IL_002e: call instance class [Jypeli]Jypeli.Camera [Jypeli]Jypeli.Game::get_Camera()
IL_0033: callvirt instance void [Jypeli]Jypeli.Camera::ZoomToLevel()
// }
IL_0038: nop
IL_0039: ret
} // end of method Lumiukko::Begin
Tämä koodi on CIL-välikieli, johon meidän Begin()
-ohjelma on käännetty. Mukana on laitettu kommentteina C#-ohjelman eri osat.
CIL-ohjelma koostuu yksinkertaisista komennoista, jotka suoritetaan samassa järjestyksessä kuin C# (eli ylhäältä alas). Yksi rivi C#-koodia vastaa useampaa CIL-välikielen komennoista. Esimerkiksi Begin
-aliohjelmassa olevat kaksi riviä
vastaavat CIL-välikieltä
ldc.r8 10
ldc.r8 10
ldsfld class [Jypeli]Jypeli.Ellipse [Jypeli]Jypeli.Shape::Circle
newobj instance void [Jypeli]Jypeli.GameObject::.ctor(float64, float64, class [Jypeli]Jypeli.Shape)
stloc.0
ldarg.0
ldloc.0
call instance void [Jypeli]Jypeli.Game::Add(class [Jypeli]Jypeli.IGameObject)
(tulosteesta siivottu turhat kommentit ja rivinumerot pois).
CIL-kieli on dokumentoitu hyvin laajasti ja on mahdollista lukea käsin. CIL-kieli on pinokieli, eli muuttujien sijaan kaikki tieto välitetään pinon kautta. Pinoa voi ajatella tässä tapauksessa korttipakkana: esimerkiksi ld
- eli "load"-komennot laitavat "pakan päälle" jonkun arvon. Monet muut komennot puolestaan ottavat pinon päältä arvoja ja tekevät niillä asioita.
Tällä logiikalla yllä oleva koodipätkä voi tulkita pienellä vaivalla seuraavasti:
// Lataa pinoon luku 10 8-tavuisena reaalilukuna (eli double)
ldc.r8 10
// Lataa pinoon luku 10 8-tavuisena reaalilukuna (eli double)
ldc.r8 10
// Lataa pinoon arvo Shape.Circle
ldsfld class [Jypeli]Jypeli.Ellipse [Jypeli]Jypeli.Shape::Circle
// Ota pinosta 3 päällimmäistä arvoa (luku 10.0, luku 10.0 ja Shape.Circle), tee niillä uusi GameObject ja laita se pinon päälle
newobj instance void [Jypeli]Jypeli.GameObject::.ctor(float64, float64, class [Jypeli]Jypeli.Shape)
// Ota pinosta päällimmäinen arvo (luotu GameObject) ja tallenna se paikalliseen muuttujaan 0 (eli p1 C#-koodissa)
stloc.0
// Lataa pinoon nykyinen peli (Lumiukko)
ldarg.0
// Lataa pinoon paikallinen muuttuja 0 (eli p1 C#-koodissa, joka on GameObject)
ldloc.0
// Ota pinosta yksi arvo (eli GameObject) sekä peli, johon halutaan vaikuttaa (Lumiukko) ja kutsu Add-aliohjelma
call instance void [Jypeli]Jypeli.Game::Add(class [Jypeli]Jypeli.IGameObject)
Eri komentojen toiminnot voi katsoa suoraan ILSpy-ophjelmasta viemällä kursori komennon päälle:
These are the current permissions for this document; please modify if needed. You can always modify these permissions from the manage page.