MSLNZ / msl-loadlib

Load a shared library (and access a 32-bit library from 64-bit Python)
MIT License
75 stars 17 forks source link

Feature wish: access the property defined in .Net library #46

Closed dwang-git closed 1 week ago

dwang-git commented 2 months ago

Is it possible to have a simple way to access the properties defined in .Net library? I have the question also posted in stackoverflow: https://stackoverflow.com/questions/78880602/msl-loadlib-access-the-property-defined-in-net-library

Thanks!

jborbely commented 2 months ago

Yes, it is possible.

Suppose the .NET code is

// issue46.cs
using System;

namespace Issue46
{
    public class Example
    {
        private int _number;

        public int Number
        {
            get
            {
                return this._number;
            }
            set
            {
                this._number = value;
            }
        }
    }
}

Also, suppose that the .NET library is compiled to a file named issue46.dll, using the command

csc.exe /platform:x86 /target:library issue46.cs

I'll show two ways that you could implement the Python Client to get/set the .NET property

from msl.loadlib import Client64, Server32

class Server(Server32):

    def __init__(self, host, port):
        super().__init__("issue46.dll", "net", host, port)
        self._example = self.lib.Issue46.Example()

    def get_Number(self):
        return self._example.Number

    def set_Number(self, number):
        self._example.Number = number

class ClientGetSet(Client64):

    def __init__(self):
        super().__init__(__file__)

    def get_Number(self):
        return self.request32("get_Number")

    def set_Number(self, number):
        self.request32("set_Number", number)

class ClientProperty(Client64):

    def __init__(self):
        super().__init__(__file__)

    @property
    def Number(self):
        return self.request32("get_Number")

    @Number.setter
    def Number(self, value):
        self.request32("set_Number", value)

if __name__ == "__main__":
    with ClientGetSet() as c:
        print("Using explicit get/set methods")
        print(f"Number={c.get_Number()}")
        c.set_Number(46)
        print(f"Number={c.get_Number()}")

    with ClientProperty() as c:
        print("Using @property decorator")
        print(f"Number={c.Number}")
        c.Number = 12
        print(f"Number={c.Number}")
dwang-git commented 2 months ago

@jborbely, Thanks for your suggestions. Yeah, I can see it would work. However, as there are many properties in the C# dll need to be accessed from the client, I wonder whether there is any cleaner way to do that to avoid defining a function for each property in the server class (here get_Number/set_Number correspond to one property "Number" in the Example object).

jborbely commented 2 months ago

Example .NET library

// issue46.cs
using System;

namespace Issue46 {
    public class Example {

        private int _integer;
        private string _string;

        public int Integer {
            get { return this._integer; }
            set { this._integer = value; }
        }

        public string String {
            get { return this._string; }
            set { this._string = value; }
        }
    }
}

Compile example library

csc.exe /platform:x86 /target:library issue46.cs

Example Python script to get/set any property in the example library

# issue46.py
from msl.loadlib import Client64, Server32

class Server(Server32):
    def __init__(self, host, port):
        super().__init__("issue46.dll", "net", host, port)
        self._example = self.lib.Issue46.Example()

    def get_property(self, name):
        print(f"Server32.get({name})")
        return getattr(self._example, name)

    def set_property(self, name, value):
        print(f"Server32.set({name}, {value})")
        setattr(self._example, name, value)

class Client(Client64):
    def __init__(self):
        super().__init__(__file__)

    def __getattr__(self, name):
        return self.request32("get_property", name)

    def __setattr__(self, name, value):
        if name.startswith("_"):
            super().__setattr__(name, value)
        else:
            self.request32("set_property", name, value)

if __name__ == "__main__":
    c = Client()
    print(f"{c.Integer=}")
    print(f"{c.String=}")
    c.Integer = 46
    c.String = "issue46"
    print(f"{c.Integer=}")
    print(f"{c.String=}")
    stdout, stderr = c.shutdown_server32()
    print(stdout.read().decode())

Python output

(.venv) ..\issue46> py .\issue46.py
c.Integer=0
c.String=None
c.Integer=46
c.String='issue46'
Server32.get(Integer)
Server32.get(String)
Server32.set(Integer, 46)
Server32.set(String, issue46)
Server32.get(Integer)
Server32.get(String)

Adding print statements on the Server are just to show that setting a property actually sets it in the .NET library (and not simply creates a new c.Attribute in your 64-bit Python Client instance). I would not keep the print statements as the stdout buffer will get filled (at 4096 characters) and that may cause communication to get blocked.

dwang-git commented 2 months ago

Thank you @jborbely ! Yeah, that works for me.

I met another problem when accessing the property declared as an interface in .Net library. So I'm having two .Net libraries, one depends on the other (say library A depends on B). In my server code, I only wrapped around library A, but one of the property of A is of interface type, which is defined in library B. So every time when I access that property, there would be the error "TypeError: cannot pickle 'xxx' object". Is there any way to fix or work around this? Thanks!

jborbely commented 2 months ago

It is not possible to return an object that resides in memory from the server, since a 32-bit process and a 64-bit process cannot share memory space. So there is no way to fix it, only work around it. Essentially, the interface type must stay on the server and the client must interact with the interface via objects that can be pickled.

Look at #39 to see if that is helpful to see how to deal with a similar case where that library returned objects that must stay in 32-bit memory space.

dwang-git commented 2 months ago

Thanks @jborbely for your quick response, and the fix in #39 works for me. Really appreciate it! I still have another problem: there is one property (name "cx") of type comobject. When I access the property of cx, it will have exception "AttributeError: 'ComObject' object has no attribute 'Type'". I checked in the C# program, it can access that property of cx without problem. I attached the watch info from Visual Studio of "cx". How can I get it in the python server? Thanks!

Screenshot from 2024-08-20 23-20-56

jborbely commented 2 months ago

I don't have experience accessing a System.__ComObject from .NET, so I don't know how to fix this. What I can do is provide some additional information that may help you find a solution.