[Python] Reuse chip.native to get server lib handle (#31387)

* [Python] Reuse chip.native to get server lib handle

* Raise on initialization error

* Post-review update

* Fix for Python < 3.9
diff --git a/src/controller/python/BUILD.gn b/src/controller/python/BUILD.gn
index 4b84c63..c8d2ac3 100644
--- a/src/controller/python/BUILD.gn
+++ b/src/controller/python/BUILD.gn
@@ -269,10 +269,7 @@
       if (chip_controller) {
         sources += [ "chip/ChipDeviceCtrl.py" ]
       } else {
-        sources += [
-          "chip/server/__init__.py",
-          "chip/server/types.py",
-        ]
+        sources += [ "chip/server/__init__.py" ]
       }
     },
     {
diff --git a/src/controller/python/chip/ChipStack.py b/src/controller/python/chip/ChipStack.py
index d64fa34..6df7e41 100644
--- a/src/controller/python/chip/ChipStack.py
+++ b/src/controller/python/chip/ChipStack.py
@@ -448,7 +448,7 @@
     def _loadLib(self):
         if self._ChipStackLib is None:
             self._ChipStackLib = chip.native.GetLibraryHandle()
-            self._chipDLLPath = chip.native.FindNativeLibraryPath()
+            self._chipDLLPath = chip.native.FindNativeLibraryPath(chip.native.Library.CONTROLLER)
 
             self._ChipStackLib.pychip_DeviceController_StackInit.argtypes = [c_void_p, c_bool]
             self._ChipStackLib.pychip_DeviceController_StackInit.restype = PyChipError
diff --git a/src/controller/python/chip/native/__init__.py b/src/controller/python/chip/native/__init__.py
index 7801129..ce8b762 100644
--- a/src/controller/python/chip/native/__init__.py
+++ b/src/controller/python/chip/native/__init__.py
@@ -1,14 +1,34 @@
+#
+#    Copyright (c) 2021 Project CHIP Authors
+#
+#    Licensed under the Apache License, Version 2.0 (the "License");
+#    you may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+#
+
 import ctypes
 import enum
 import glob
 import os
 import platform
 import typing
+from dataclasses import dataclass
 
 import chip.exceptions
 import construct
 
-NATIVE_LIBRARY_BASE_NAME = "_ChipDeviceCtrl.so"
+
+class Library(enum.Enum):
+    CONTROLLER = "_ChipDeviceCtrl.so"
+    SERVER = "_ChipServer.so"
 
 
 def _AllDirsToRoot(dir):
@@ -119,7 +139,22 @@
         return not self == other
 
 
-def FindNativeLibraryPath() -> str:
+PostAttributeChangeCallback = ctypes.CFUNCTYPE(
+    None,
+    ctypes.c_uint16,
+    ctypes.c_uint16,
+    ctypes.c_uint16,
+    ctypes.c_uint8,
+    ctypes.c_uint16,
+    # TODO: This should be a pointer to uint8_t, but ctypes does not provide
+    #       such a type. The best approximation is c_char_p, however, this
+    #       requires the caller to pass NULL-terminate C-string which might
+    #       not be the case here.
+    ctypes.c_char_p,
+)
+
+
+def FindNativeLibraryPath(library: Library) -> str:
     """Find the native CHIP dll/so path."""
 
     scriptDir = os.path.dirname(os.path.abspath(__file__))
@@ -129,7 +164,7 @@
     # modules.
     dmDLLPath = os.path.join(
         os.path.dirname(scriptDir),  # file should be inside 'chip'
-        NATIVE_LIBRARY_BASE_NAME)
+        library.value)
     if os.path.exists(dmDLLPath):
         return dmDLLPath
 
@@ -143,7 +178,7 @@
         "build",
         buildMachineGlob,
         "src/controller/python/.libs",
-        NATIVE_LIBRARY_BASE_NAME,
+        library.value,
     )
     for dir in _AllDirsToRoot(scriptDir):
         dmDLLPathGlob = os.path.join(dir, relDMDLLPathGlob)
@@ -152,8 +187,7 @@
                 return dmDLLPath
 
     raise Exception(
-        "Unable to locate Chip Device Manager DLL (%s); expected location: %s" %
-        (NATIVE_LIBRARY_BASE_NAME, scriptDir))
+        f"Unable to locate CHIP DLL ({library.value}); expected location: {scriptDir}")
 
 
 class NativeLibraryHandleMethodArguments:
@@ -168,22 +202,38 @@
         method.argtype = argumentTypes
 
 
-_nativeLibraryHandle: ctypes.CDLL = None
+@dataclass
+class _Handle:
+    dll: ctypes.CDLL = None
+    initialized: bool = False
 
 
-def _GetLibraryHandle(shouldInit: bool) -> ctypes.CDLL:
-    """Get a memoized handle to the chip native code dll."""
+_nativeLibraryHandles: typing.Dict[Library, _Handle] = {}
 
-    global _nativeLibraryHandle
-    if _nativeLibraryHandle is None:
-        if shouldInit:
-            raise Exception("Common stack has not been initialized!")
-        _nativeLibraryHandle = ctypes.CDLL(FindNativeLibraryPath())
-        setter = NativeLibraryHandleMethodArguments(_nativeLibraryHandle)
-        setter.Set("pychip_CommonStackInit", PyChipError, [ctypes.c_char_p])
-        setter.Set("pychip_FormatError", None, [ctypes.POINTER(PyChipError), ctypes.c_char_p, ctypes.c_uint32])
 
-    return _nativeLibraryHandle
+def _GetLibraryHandle(lib: Library, expectAlreadyInitialized: bool) -> ctypes.CDLL:
+    """Get a memoized _Handle to the chip native code dll."""
+
+    global _nativeLibraryHandles
+    if lib not in _nativeLibraryHandles:
+
+        handle = _Handle(ctypes.CDLL(FindNativeLibraryPath(lib)))
+        _nativeLibraryHandles[lib] = handle
+
+        setter = NativeLibraryHandleMethodArguments(handle.dll)
+        if lib == Library.CONTROLLER:
+            setter.Set("pychip_CommonStackInit", PyChipError, [ctypes.c_char_p])
+            setter.Set("pychip_FormatError", None,
+                       [ctypes.POINTER(PyChipError), ctypes.c_char_p, ctypes.c_uint32])
+        elif lib == Library.SERVER:
+            setter.Set("pychip_server_native_init", PyChipError, [])
+            setter.Set("pychip_server_set_callbacks", None, [PostAttributeChangeCallback])
+
+    handle = _nativeLibraryHandles[lib]
+    if expectAlreadyInitialized and not handle.initialized:
+        raise Exception("CHIP handle has not been initialized!")
+
+    return handle
 
 
 def Init(bluetoothAdapter: int = None):
@@ -194,12 +244,15 @@
     params.BluetoothAdapterId = bluetoothAdapter if bluetoothAdapter is not None else 0
     params = CommonStackParams.build(params)
 
-    _GetLibraryHandle(False).pychip_CommonStackInit(ctypes.c_char_p(params))
+    handle = _GetLibraryHandle(Library.CONTROLLER, False)
+    handle.dll.pychip_CommonStackInit(ctypes.c_char_p(params)).raise_on_error()
+    handle.initialized = True
 
 
 class HandleFlags(enum.Flag):
     REQUIRE_INITIALIZATION = enum.auto()
 
 
-def GetLibraryHandle(flags=HandleFlags.REQUIRE_INITIALIZATION):
-    return _GetLibraryHandle(HandleFlags.REQUIRE_INITIALIZATION in flags)
+def GetLibraryHandle(flags=HandleFlags.REQUIRE_INITIALIZATION) -> ctypes.CDLL:
+    handle = _GetLibraryHandle(Library.CONTROLLER, HandleFlags.REQUIRE_INITIALIZATION in flags)
+    return handle.dll
diff --git a/src/controller/python/chip/server/ServerInit.cpp b/src/controller/python/chip/server/ServerInit.cpp
index 611d494..39edd7a 100644
--- a/src/controller/python/chip/server/ServerInit.cpp
+++ b/src/controller/python/chip/server/ServerInit.cpp
@@ -27,6 +27,7 @@
 #include <setup_payload/QRCodeSetupPayloadGenerator.h>
 #include <setup_payload/SetupPayload.h>
 
+#include <controller/python/chip/native/PyChipError.h>
 #include <credentials/examples/DeviceAttestationCredsExample.h>
 
 // #include <support/CHIPMem.h>
@@ -118,21 +119,11 @@
     gPythonServerDelegate.SetPostAttributeChangeCallback(cb);
 }
 
-void pychip_server_native_init()
+PyChipError pychip_server_native_init()
 {
-    CHIP_ERROR err = CHIP_NO_ERROR;
 
-    err = chip::Platform::MemoryInit();
-    if (err != CHIP_NO_ERROR)
-    {
-        ChipLogError(DeviceLayer, "Failed to initialize CHIP stack: memory init failed: %s", chip::ErrorStr(err));
-    }
-
-    err = chip::DeviceLayer::PlatformMgr().InitChipStack();
-    if (err != CHIP_NO_ERROR)
-    {
-        ChipLogError(DeviceLayer, "Failed to initialize CHIP stack: platform init failed: %s", chip::ErrorStr(err));
-    }
+    PyReturnErrorOnFailure(ToPyChipError(Platform::MemoryInit()));
+    PyReturnErrorOnFailure(ToPyChipError(DeviceLayer::PlatformMgr().InitChipStack()));
 
     static chip::DeviceLayer::TestOnlyCommissionableDataProvider TestOnlyCommissionableDataProvider;
     chip::DeviceLayer::SetCommissionableDataProvider(&TestOnlyCommissionableDataProvider);
@@ -169,24 +160,20 @@
 
     // Init ZCL Data Model and CHIP App Server
     static chip::CommonCaseDeviceServerInitParams initParams;
-    (void) initParams.InitializeStaticResourcesBeforeServerInit();
+    PyReturnErrorOnFailure(ToPyChipError(initParams.InitializeStaticResourcesBeforeServerInit()));
     initParams.operationalServicePort        = CHIP_PORT;
     initParams.userDirectedCommissioningPort = CHIP_UDC_PORT;
 
-    chip::Server::GetInstance().Init(initParams);
+    PyReturnErrorOnFailure(ToPyChipError(chip::Server::GetInstance().Init(initParams)));
 
     // Initialize device attestation config
     SetDeviceAttestationCredentialsProvider(chip::Credentials::Examples::GetExampleDACProvider());
 
-    err = chip::DeviceLayer::PlatformMgr().StartEventLoopTask();
-    if (err != CHIP_NO_ERROR)
-    {
-        ChipLogError(DeviceLayer, "Failed to initialize CHIP stack: platform init failed: %s", chip::ErrorStr(err));
-    }
+    PyReturnErrorOnFailure(ToPyChipError(chip::DeviceLayer::PlatformMgr().StartEventLoopTask()));
 
     atexit(CleanShutdown);
 
-    return /*err*/;
+    return ToPyChipError(CHIP_NO_ERROR);
 }
 }
 
diff --git a/src/controller/python/chip/server/__init__.py b/src/controller/python/chip/server/__init__.py
index d0b046f..8280ec0 100644
--- a/src/controller/python/chip/server/__init__.py
+++ b/src/controller/python/chip/server/__init__.py
@@ -1,91 +1,32 @@
+#
+#    Copyright (c) 2021 Project CHIP Authors
+#
+#    Licensed under the Apache License, Version 2.0 (the "License");
+#    you may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+#
+
 import ctypes
-import glob
-import os
-import platform
 
-from chip.server.types import PostAttributeChangeCallback
-
-NATIVE_LIBRARY_BASE_NAME = "_ChipServer.so"
-
-
-def _AllDirsToRoot(dir):
-    """Return all parent paths of a directory."""
-    dir = os.path.abspath(dir)
-    while True:
-        yield dir
-        parent = os.path.dirname(dir)
-        if parent == "" or parent == dir:
-            break
-        dir = parent
-
-
-def FindNativeLibraryPath() -> str:
-    """Find the native CHIP dll/so path."""
-
-    scriptDir = os.path.dirname(os.path.abspath(__file__))
-
-    # When properly installed in the chip package, the Chip Device Manager DLL will
-    # be located in the package root directory, along side the package's
-    # modules.
-    dmDLLPath = os.path.join(
-        os.path.dirname(scriptDir),  # file should be inside 'chip'
-        NATIVE_LIBRARY_BASE_NAME,
-    )
-    if os.path.exists(dmDLLPath):
-        return dmDLLPath
-
-    # For the convenience of developers, search the list of parent paths relative to the
-    # running script looking for an CHIP build directory containing the Chip Device
-    # Manager DLL. This makes it possible to import and use the ChipDeviceMgr module
-    # directly from a built copy of the CHIP source tree.
-    buildMachineGlob = "%s-*-%s*" % (platform.machine(),
-                                     platform.system().lower())
-    relDMDLLPathGlob = os.path.join(
-        "build",
-        buildMachineGlob,
-        "src/controller/python/.libs",
-        NATIVE_LIBRARY_BASE_NAME,
-    )
-    for dir in _AllDirsToRoot(scriptDir):
-        dmDLLPathGlob = os.path.join(dir, relDMDLLPathGlob)
-        for dmDLLPath in glob.glob(dmDLLPathGlob):
-            if os.path.exists(dmDLLPath):
-                return dmDLLPath
-
-    raise Exception(
-        "Unable to locate Chip Server DLL (%s); expected location: %s"
-        % (NATIVE_LIBRARY_BASE_NAME, scriptDir)
-    )
-
-
-class NativeLibraryHandleMethodArguments:
-    """Convenience wrapper to set native method argtype and restype for methods."""
-
-    def __init__(self, handle):
-        self.handle = handle
-
-    def Set(self, methodName: str, resultType, argumentTypes: list):
-        method = getattr(self.handle, methodName)
-        method.restype = resultType
-        method.argtype = argumentTypes
-
-
-_nativeLibraryHandle: ctypes.CDLL = None
+from chip import native
+from chip.native import PostAttributeChangeCallback
 
 
 def GetLibraryHandle(cb: PostAttributeChangeCallback) -> ctypes.CDLL:
     """Get a memoized handle to the chip native code dll."""
 
-    global _nativeLibraryHandle
-    if _nativeLibraryHandle is None:
-        _nativeLibraryHandle = ctypes.CDLL(FindNativeLibraryPath())
+    handle = native._GetLibraryHandle(native.Library.SERVER, False)
+    if not handle.initialized:
+        handle.dll.pychip_server_native_init().raise_on_error()
+        handle.dll.pychip_server_set_callbacks(cb)
+        handle.initialized = True
 
-        setter = NativeLibraryHandleMethodArguments(_nativeLibraryHandle)
-        setter.Set("pychip_server_native_init", None, [])
-        setter.Set("pychip_server_set_callbacks",
-                   None, [PostAttributeChangeCallback])
-
-        _nativeLibraryHandle.pychip_server_native_init()
-        _nativeLibraryHandle.pychip_server_set_callbacks(cb)
-
-    return _nativeLibraryHandle
+    return handle.dll
diff --git a/src/controller/python/chip/server/types.py b/src/controller/python/chip/server/types.py
deleted file mode 100644
index 3df693d..0000000
--- a/src/controller/python/chip/server/types.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from ctypes import CFUNCTYPE, c_char_p, c_uint8, c_uint16
-
-PostAttributeChangeCallback = CFUNCTYPE(
-    None,
-    # py_object,
-    c_uint16,
-    c_uint16,
-    c_uint16,
-    c_uint8,
-    c_uint16,
-    c_char_p,
-)