Du bist nicht angemeldet.

Stilllegung des Forums
Das Forum wurde am 05.06.2023 nach über 20 Jahren stillgelegt (weitere Informationen und ein kleiner Rückblick).
Registrierungen, Anmeldungen und Postings sind nicht mehr möglich. Öffentliche Inhalte sind weiterhin zugänglich.
Das Team von spieleprogrammierer.de bedankt sich bei der Community für die vielen schönen Jahre.
Wenn du eine deutschsprachige Spieleentwickler-Community suchst, schau doch mal im Discord und auf ZFX vorbei!

Werbeanzeige

1

14.12.2007, 15:52

[C#] Dynamische Klassen zur Laufzeit erstellen

Hi,

ich möchte gerne zur Laufzeit Methoden, Felder und ganze Klassen erstellen und verwenden können. Einige Stichwörter dazu sind wohl

"typebuilder"
"methodbuilder"
.

Hat sich damit schon mal jemand beschäftigt und weiß vielleicht gute Tutorials oder kann sagen, worauf es dabei ankommt?

Am liebsten wäre mir, wenn es eine einfache Möglichkeit gäbe, eine cs-Klassendatei zur Laufzeit zu kompilieren. Unter den oben angegebenen Stichwörtern ist sowas anscheinend nicht vorgesehen - die Bspe. sehen recht kompliziert aus.

Dank und Gruß!
Passwort:

David_pb

Community-Fossil

Beiträge: 3 886

Beruf: 3D Graphics Programmer

  • Private Nachricht senden

2

15.12.2007, 12:11

Siehe dazu:

:arrow: System.CodeDom
:arrow: System.CodeDom.Compiler

Kurzer Überblick

Die Klasse CSharpCodeProvider bietet die Möglichkeit Code zu compilieren, wofür es drei Möglichkeiten gibt. Der Code kann direkt übergeben werden, eine DOM-Struktur kann zugrunde liegen oder eine Datei wird angegeben.

Hier ein Beispiel:

C-/C++-Quelltext

1
2
CSharpCodeProvider provider = new CSharpCodeProvider();
result = provider.CompileAssemblyFromSource( options, source );


Der Compiler benötigt natürlich aber noch weitere Informationen. Zum Beispiel die referenzierten assemblies:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
CompilerParameters options = new CompilerParameters();
 
options.OutputAssembly = "foo";
options.ReferencedAssemblies.Add( "System.dll" );
options.ReferencedAssemblies.Add( "System.Windows.Forms.dll" );
options.CompilerOptions = "/t:library";
options.GenerateInMemory = true;
options.GenerateExecutable = false;
options.IncludeDebugInformation = false;


Hier werden angegeben:
:arrow: Der Name des Kompilats
:arrow: Eine Liste der benötigten Assembly-Referenzen
:arrow: Compilieroptionen
:arrow: Das Kompilat im Speicher erzeugen
:arrow: Keine Ausführbare Datei des Kompilats
:arrow: Keine Debuginformationen

Zurück gibt der Compiler ein Objekt vom Typ CompilerResults. Dieses Objekt enthält alle Informationen des Kompiliervorgangs (u.a. auch Übersetzungsfehler):

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
result = provider.CompileAssemblyFromSource( options, source );

if ( result.Errors.Count > 0 )
{
                foreach ( CompilerError error in result.Errors )
                {
                    listBox2.Items.Add( error.ToString() );
                }
}
else
{
                listBox2.Items.Add( "Erfolgreich übersetzt" );
                asm = result.CompiledAssembly;
}


Die Beispielfunktion zum Kompilieren sieht so aus:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
private Assembly Compile( string source )
{
        Assembly asm = null;
        CompilerResults result = null;
        CompilerParameters options = new CompilerParameters();
 
        try
        {
            options.OutputAssembly = "foo";
            options.ReferencedAssemblies.Add( "System.dll" );
            options.ReferencedAssemblies.Add( "System.Windows.Forms.dll" );
            options.CompilerOptions = "/t:library";
            options.GenerateInMemory = true;
            options.GenerateExecutable = false;
            options.IncludeDebugInformation = false;

            CSharpCodeProvider provider = new CSharpCodeProvider();
            result = provider.CompileAssemblyFromSource( options, source );

            if ( result.Errors.Count > 0 )
            {
                foreach ( CompilerError error in result.Errors )
                {
                    listBox2.Items.Add( error.ToString() );
                }
            }
            else
            {
                listBox2.Items.Add( "Erfolgreich übersetzt" );
                asm = result.CompiledAssembly;
            }
        }
        catch
        {}

        return asm;
}


Das ist nun natürlich noch extrem unflexibel. Aber für ein Beispiel ausreichend!

Wenn erfolgreich kompiliert wurde kann eine Instanz aus dem zurückgegebenen Assemblyobjekt erzeugt werden:

C-/C++-Quelltext

1
mInstance = asm.CreateInstance( "WindowsApplication1.foo", true, BindingFlags.Instance | BindingFlags.Public, null, new object[] { 100 }, CultureInfo.CurrentCulture, null );



Dieses Objekt ist, falls alles korrekt lief, eine Instanz einer Klasse im eben erzeugten Assembly (hier "foo" im Namespace "WindowsApplication1"). Nun ist diese aber nur als Objekt verfügbar, was also tun?
Über die Methode Objekt::GetType() können Typeninformationen gesucht werden. Zum Beispiel alle öffentlichen Methoden:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Type t = mInstance.GetType();

MethodInfo[] methods = t.GetMethods();
foreach ( MethodInfo method in methods )
{
            StringBuilder builder = new StringBuilder();
            builder.AppendFormat( "{0} {1}( ", method.ReturnType.ToString(), method.Name );
            ParameterInfo[] @params = method.GetParameters();

            int i = 0;
            foreach ( ParameterInfo parameter in @params )
            {
                        builder.AppendFormat("{0}{1} ", parameter.ToString(), ( i < @params.Length-1 ? "," : "" ) );
                        i++;
            }

            builder.Append( ")" );
            listBox1.Items.Add( builder.ToString() );
}


Also nochmal die Beispielfunktion im Ganzen:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private void button1_Click( object sender, EventArgs e )
{
            listBox2.Items.Clear();
            Assembly asm = Compile( textBox1.Text );

            if ( asm == null )
                return;

            mInstance = asm.CreateInstance( "WindowsApplication1.foo", true, BindingFlags.Instance | BindingFlags.Public, null, new object[] { 100 }, CultureInfo.CurrentCulture, null );
            Type t = mInstance.GetType();

            MethodInfo[] methods = t.GetMethods();
            foreach ( MethodInfo method in methods )
            {
                StringBuilder builder = new StringBuilder();
                builder.AppendFormat( "{0} {1}( ", method.ReturnType.ToString(), method.Name );
                ParameterInfo[] @params = method.GetParameters();

                int i = 0;
                foreach ( ParameterInfo parameter in @params )
                {
                    builder.AppendFormat("{0}{1} ", parameter.ToString(), ( i < @params.Length-1 ? "," : "" ) );
                    i++;
                }

                builder.Append( ")" );
                listBox1.Items.Add( builder.ToString() );
        }
}


Auch unflexibel ohne Ende. Aber Beispielhaft tauglich! :)

Zu guter Letzt will man natürlich auch diverse Methoden, Eigenschaften (etcpp) aufrufen können. Das geht über die Klasse "Type" und ihre Diversen Methoden (GetMethod, GetProperty, ...).

C-/C++-Quelltext

1
MethodInfo method = mInstance.GetType().GetMethod( name, BindingFlags.Public | BindingFlags.Instance, null, Type.GetTypeArray( parameters ), null );


Hat man die Methoden-Information (wurde nichts gefunden wird "null" zurückgegeben) so kann man über MethodInfo::Invoke die Methode aufrufen. (Equivalent geht das auch mit z.B. PropertyInfo).

C-/C++-Quelltext

1
2
3
4
if ( method != null )
{
            method.Invoke( mInstance, parameters );
}


Und nochmal in einem Stück:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
private void Execute( string name, params object[] parameters )
{
    if ( mInstance == null )
        return;

    MethodInfo method = mInstance.GetType().GetMethod( name, BindingFlags.Public | BindingFlags.Instance, null, Type.GetTypeArray( parameters ), null );
    if ( method != null )
    {
        method.Invoke( mInstance, parameters );
    }
    else
        listBox2.Items.Add( string.Format( "Methode nicht gefunden: {0}", name ) );
}


Aufgerufen wird die Funktion dann z.B. über:

C-/C++-Quelltext

1
2
3
Execute( "MyFunction" ); // kein Parameter

Execute( "MyFunction", 10 ); // ein Parameter (Int32)

Execute( "MyFunction", "Hallo", 150 ); // zwei Parameter (String, Int32)


Zum Schluß noch meine Testklasse:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using System;
using System.Windows.Forms;    

namespace WindowsApplication1
{
    class foo
    {
        private int x;

        public foo( int value )
        {
            x = value;
        }

        public void SayHello()
        {
            MessageBox.Show( string.Format( "foo::SayHello() -> {0}", x ) );
        }

        public void HelloAgain( string name )
        {
            MessageBox.Show( string.Format( "Hello {0}", name ) );
        }

        public void Add( int a, int b )
        {
            MessageBox.Show( string.Format( "{0}+{1}={3}", a, b, a+b ) );
        }
    }
}
@D13_Dreinig

3

15.12.2007, 14:44

Verstehe ich das richtig, daß Du den Quellcode der Beispielklasse "foo" zur Laufzeit aus einer Datei geladen und kompiliert, ein Objekt davon erzeugt und die Methoden davon verwendet hast ?! Das ist genau das was ich meinte!

...bin begeistert - ist ja ein richtiges kleines Tutorial! Vielen Dank erst mal!!!

Werds nachher gleich ausprobieren....

David_pb

Community-Fossil

Beiträge: 3 886

Beruf: 3D Graphics Programmer

  • Private Nachricht senden

4

15.12.2007, 15:58

Zitat von »"Passwort:"«

Verstehe ich das richtig, daß Du den Quellcode der Beispielklasse "foo" zur Laufzeit aus einer Datei geladen und kompiliert, ein Objekt davon erzeugt und die Methoden davon verwendet hast ?!


Yo, richtig verstanden!
@D13_Dreinig

5

17.12.2007, 13:42

...funktioniert alles wunderbar - Danke noch mal! Damit ergeben sich natürlich ganz neue Möglichkeiten...

Nur eine Sache hab ich noch nicht hingekriegt: wenn ich mit

C-/C++-Quelltext

1
options.ReferencedAssemblies.Add( "Microsoft.DirectX.dll" );
eine Referenz auf DirectX zu setzen versuche, beschwert er sich, daß er das nicht finden kann - die Datei ist aber vorhanden. Vielleicht kennt er den Pfad nicht? Die Referenzen von System-Assemblies funktionieren - z.B.:

C-/C++-Quelltext

1
options.ReferencedAssemblies.Add( "System.Windows.Forms.dll" );


:?: Wie kann man options vom Typ CompilerParameters den Pfad einer Referenzierung angeben?

David_pb

Community-Fossil

Beiträge: 3 886

Beruf: 3D Graphics Programmer

  • Private Nachricht senden

6

17.12.2007, 14:04

Im Zweifel fügst du eine Assemblyreferenz in ein Testprojekt ein und schaust dir den korrekten Namen unter den Eigenschaften an.
@D13_Dreinig

Werbeanzeige