2022-04-17

71: Create Any Global UNO Service in Python

<The previous article in this series | The table of contents of this series | The next article in this series>

In order to instantiate the UNO component from a remote programming language environment or to create an artifact like a spread sheet cell function.

Topics


About: UNO (Universal Network Objects)
About: LibreOffice
About: Apache OpenOffice
About: the Python programming language

The table of contents of this article


Starting Context



Target Context


  • The reader will know how to create his or her own global UNO service in Python.

Orientation


There is an article on how to create and register any UNO Interface and generate the mapping images.

There is an article on how to build any sample program of this series.

There is an article on how to build an environment for developing UNO programs in Linux or in Windows.


Main Body

Stage Direction
Here are Special-Student-7, Lenard (a Python programmer), and Jane (a Python programmer) in front of a computer.


0: What We Mean by "UNO Service" Here


Special-Student-7
What we mean by "UNO service" here is what is explained in an article.

To state succinctly, UNO service is an item in an UNO objects factory, which is called 'UNO services manager'.

In fact, "service" is a very confusedly and confusingly used term in the official terminology, and we do not mean the 2nd meaning or the 3rd meaning in the official terminology by "UNO service".

If you are wondering how you can create a "service" in the 2nd meaning in the official terminology, I am pretty sure that it is not something you need to create.

Some people may deem "service" in the 3rd meaning in the official terminology handy, but also it is not something you particularly need to create, and as I choose not to create any, I do not explain about it.


1: Why Is a UNO Service Desired?


Special-Student-7
A purpose of creating a UNO service is to instantiate a UNO component from a remote programming language environment.

You know, in the local programming language environment, the UNO component can be instantiated as a normal Python class, but from a remote programming language environment, there must be another way.

Jane
I don't understand what is "local" and what is "remote"; you know, a place is "local" if I'm in it and "remote" if I'm looking at it from afar; nothing is particularly "local" or "remote" by itself; it's just about where I am.

Special-Student-7
You are right. "local" here means the programming language environment in which the UNO component is instantiated; your location does not matter in this case.

Lenard
But my UNO components can be instantiated by one of my UNO objects, local, according to your term.

Special-Student-7
But your 1st UNO object has to come from somewhere, and it typically comes from a UNO services manager, which means that the 1st UNO object comes into being as a UNO service instance.

Lenard
But I can make my Python macro (local) instantiate my Python UNO component, with the macro being called from a remote programming language environment.

Special-Student-7
Ah, you are right; that is a way, certainly.

So, UNO service is a way to instantiate a UNO component from a remote programming language environment, not the only way.

As far as I know, the more prevalent motivation for creating a Python UNO service is to create an artifact that requires the definition of a UNO service

Jane
"an artifact"? I don't understand what you are talking about.

Special-Student-7
"an artifact" is, for example, a spread sheet cell function, which is really a method of a UNO component, which is instantiated as a UNO service by the office main body.

Jane
So, I need to know how to create a UNO service if I need to create a spread sheet cell function . . .

Special-Student-7

2: A Note: We Are Creating a Global UNO Service


Special-Student-7
Although not every UNO service is a global UNO service, when we talk about UNO service in this article, we are always talking about global UNO service.

Lenard
Oh, cruel world! Non-global UNO services are being ignored!

Special-Student-7
Any non-global UNO service is only for internal use by the office product, as far as I know, and so, you will probably have no opportunity to create a non-global UNO service or even opportunity to directly use a non-global UNO service.

Lenard
Won't I?

Special-Student-7
Probably not. So, note that when I talk about UNO service without any particular specification in this series, it is about global UNO service.


3: Create the UNO Service



3-1: Prepare the UNO Component


Special-Student-7
The UNO component has to satisfy a certain prerequisite for it to be registered as a UNO service.

Namely, it has to implement the UNO interface, 'com.sun.star.lang.XServiceInfo', like this.

@Python Source Code
from typing import Any
from typing import List
from typing import Set
from typing import cast
from collections import OrderedDict
import uno
from com.sun.star.lang import IllegalArgumentException
from com.sun.star.lang import XServiceInfo
from com.sun.star.uno import XComponentContext
from unohelper import Base as UnoBase
XTest1: Any = uno.getClass ("theBiasPlanet.unoDatumTypes.tests.XTest1")

class PythonTest1UnoComponent (UnoBase, XServiceInfo, XTest1):
	c_thisClass: type = None
	c_unoServiceNames: Set [str] = set ()
	c_unoCompoundInterfaceNames: Set [str] = set ()
	# # Add UNO service names Start
	c_unoServiceNames.add ("theBiasPlanet.tests.PythonTest1UnoComponent")
	# # Add UNO service names End
	# # Add UNO compound interface names Start
	# # Add UNO compound interface names End
	
	@staticmethod
	def setThisClassToGlobalUnoServicesProvider (a_implementationClassNameToImplementationClassToUnoServiceNamesMapMap: "OrderedDict [str, OrderedDict [type, Set [str]]]")-> None:
		l_implementationClassToUnoServiceNamesMap: "OrderedDict [type, Set [str]]" = OrderedDict ()
		l_implementationClassToUnoServiceNamesMap.update ( {PythonTest1UnoComponent.c_thisClass: PythonTest1UnoComponent.c_unoServiceNames})
		a_implementationClassNameToImplementationClassToUnoServiceNamesMapMap.update ( {"{0:s}.{1:s}".format (PythonTest1UnoComponent.c_thisClass.__module__, PythonTest1UnoComponent.c_thisClass.__name__): l_implementationClassToUnoServiceNamesMap})
	
	def __init__ (a_this: "PythonTest1UnoComponent", a_underlyingUnoObjectsContextInXComponentContext: XComponentContext, a_message: str) -> None:
		a_this.i_underlyingRemoteUnoObjectsContextInXComponentContext: XComponentContext
		# # Add the other member variables Start
		a_this.i_message: str
		# # Add the other member variables End
		
		UnoBase.__init__ (a_this)
		a_this.i_underlyingRemoteUnoObjectsContextInXComponentContext = a_underlyingUnoObjectsContextInXComponentContext
		a_this.i_message = None
		
		# initialization Start
		a_this.i_message = a_message
		if a_this.i_message is None:
			raise IllegalArgumentException ("The first argument can't be null.")
		# initialization End
	
	def __del__ (a_this: "PythonTest1UnoComponent") -> None:
		None
	
	def getImplementationName (a_this: "PythonTest1UnoComponent") -> str:
		return "{0:s}.{1:s}".format (PythonTest1UnoComponent.c_thisClass.__module__, PythonTest1UnoComponent.c_thisClass.__name__)
	
	def supportsService (a_this: "PythonTest1UnoComponent", a_unoCompoundInterfaceName: str) -> bool:
		return a_unoCompoundInterfaceName in PythonTest1UnoComponent.c_unoCompoundInterfaceNames
	
	def getSupportedServiceNames (a_this: "PythonTest1UnoComponent") -> List [str]:
		l_numberOfItems: int = len (PythonTest1UnoComponent.c_unoCompoundInterfaceNames)
		l_unoCompoundInterfaceNamesArray: List [str] = [None] * l_numberOfItems
		l_itemIndex: int = 0
		l_unoCompoundInterfaceName: str = None
		for l_unoCompoundInterfaceName in PythonTest1UnoComponent.c_unoCompoundInterfaceNames:
			l_unoCompoundInterfaceNamesArray [l_itemIndex] = l_unoCompoundInterfaceName
			l_itemIndex = l_itemIndex + 1
		return l_unoCompoundInterfaceNamesArray
	
	# # Add the methods of the implemented UNO interface Start
	def test1 (a_this: "PythonTest1UnoComponent", a_name: str)-> str:
		if a_name is None:
			raise IllegalArgumentException ("The first argument can't be null.")
		return "{0:s}, {1:s}!".format (a_this.i_message, a_name)
	# # Add the methods of the implemented UNO interface End
	
	# # Add other methods Start
	# # Add other methods End

PythonTest1UnoComponent.c_thisClass = PythonTest1UnoComponent

Lenard
. . . Of course . . .

Special-Student-7
Of course, you do not need to implement it exactly like that: you need to implement the constructor and those 'XServiceInfo' methods somehow appropriately.

Lenard
"somehow"? Whathow?

Special-Student-7
The constructor has to have that 'XComponentContext' argument there and have the appropriate initialization arguments.

Python UNO is special in not respecting implementing the 'com.sun.star.lang.XInitialization' UNO interface.

Lenard
Huh?

Special-Student-7
In Java UNO and C++ UNO, the UNO service UNO component has to implement the 'com.sun.star.lang.XInitialization' UNO interface, and the initialization arguments are passed into the 'initialize' method of the UNO interface.

In Python UNO, the initialization arguments are passed into the constructor.

Lenard
What if there are multiple arguments?

Special-Student-7
The constructor has to have multiple arguments.

Lenard
So, the number of arguments has to be predetermined . . .

Special-Student-7
Yes. The situation is different from for the other UNO programming languages, in which the arguments are passed as an array.

Lenard
Well . . .

Special-Student-7
"service" for "supportsService" and "getSupportedServiceNames" are basically not UNO service, but "service" in the 2nd meaning in the official terminology.

Jane
So, what "service"s in the 2nd meaning does the UNO component need to support?

Special-Student-7
If the UNO component is of a certain kind of artifacts, the kind requires the UNO component to support certain "services"; otherwise, the UNO component does not need to support any "service", basically.

Jane
How about non-"basically"?

Special-Student-7
As "service" is so jumbled up as a concept, sometimes, something that is basically a "service" in the 2nd meaning in the official document is opportunistically abused also in the meaning of UNO service and vice versa. I have not noticed any problem in not supporting the UNO service name in those 2 methods, but I said "basically" just in case. In fact, there will be no practical problem in supporting the UNO service name there, which you may choose.

Jane
Ah, a grown-up talk.

Special-Student-7
Managing the UNO service information in like "c_unoServiceNames" is my contrivance to not scatter such information around methods, which you do not particularly follow; and the "setThisClassToGlobalUnoServicesProvider" method is for injecting the information into the global UNO services provider, which will be created next, in order to avoid doubly writing the information in the global UNO services provider, so you do not particularly need to have the method if you want to doubly write.


3-2: Create a Global UNO Services Provider


Special-Student-7
We need to create a global UNO services provider, which is required for the UNO services to be instantiated.

In fact, Python global UNO services provider is not a class as Java global UNO services provider is, but is a module that has a 'g_ImplementationHelper' 'unohelper.ImplementationHelper' object.

Lenard
Um? So, is it just a matter of having a single object?

Special-Student-7
Yes, it is.

Lenard
Then, can't I just make the UNO component have the object?

Special-Student-7
You could, but while there may be multiple UNO components, which one should have the object?

Lenard
Any arbitrary one should.

Special-Student-7
I do no approve such asymmetric-ness.

Lenard
But I have only one UNO component.

Special-Student-7
Even if you happen to have only one UNO component, the information structure would be already skewed, because the object does not really belong there.

Lenard
Would it?

Special-Student-7
Anyway, this is a global UNO services provider, followed by my utility class.

@Python Source Code
from typing import Any
from typing import Set
from collections import OrderedDict
import uno
import unohelper
from theBiasPlanet.unoUtilities.servicesHandling.GlobalUnoServicesProviderUtility import GlobalUnoServicesProviderUtility
from theBiasPlanet.tests.models.PythonTest1UnoComponent import PythonTest1UnoComponent

c_implementationClassNameToImplementationClassToUnoServiceNamesMapMap: "OrderedDict [str, OrderedDict [type, Set [str]]]" = OrderedDict ()
PythonTest1UnoComponent.setThisClassToGlobalUnoServicesProvider (c_implementationClassNameToImplementationClassToUnoServiceNamesMapMap)
g_ImplementationHelper: Any = unohelper.ImplementationHelper () # this variable has to be of the name, because the office product searches for it.
GlobalUnoServicesProviderUtility.setUpUnoServiceRegistrationHelper (g_ImplementationHelper, c_implementationClassNameToImplementationClassToUnoServiceNamesMapMap)


@Python Source Code
from typing import Any
from typing import Set
from collections import OrderedDict

class GlobalUnoServicesProviderUtility:
	@staticmethod
	def setUpUnoServiceRegistrationHelper (a_unoServiceRegistrationHelper: Any, a_implementationClassNameToImplementationClassToUnoServiceNamesMapMap: "OrderedDict [str, OrderedDict [type, Set [str]]]")-> None:
		l_implementationClassName: str = None
		l_implementationClassToUnoServiceNamesMap: "OrderedDict [type, Set [str]]" = None
		l_implementationClass: type = None
		l_UnoServiceNames: Set [str] = None
		for l_implementationClassName, l_implementationClassToUnoServiceNamesMap in a_implementationClassNameToImplementationClassToUnoServiceNamesMapMap.items ():
			for l_implementationClass, l_UnoServiceNames in l_implementationClassToUnoServiceNamesMap.items ():
				break
			a_unoServiceRegistrationHelper.addImplementation (l_implementationClass, l_implementationClassName, tuple (l_UnoServiceNames))


I have prepared that utility class, "GlobalUnoServicesProviderUtility", because it would be uneconomical to write that code into each global UNO services provider, but if you will, of course, you can be uneconomical.

As I said in the previous sub section, the information of the UNO service is injected into the global UNO services provider by the "setThisClassToGlobalUnoServicesProvider" method.

Jane
Well, you seem to have made the code more complicated than it should be: you can make the "setThisClassToGlobalUnoServicesProvider" method put the information directly into the 'g_ImplementationHelper' object, then, such a utility class will be unnecessary.

Special-Student-7
Ah, you are right, actually.

Jane
"you are right"? Just like that?

Special-Student-7
I mean, I understand what you mean: your code will be much shorter as a whole. My code is like that because I thought that each UNO component was better not depending on the implementation of the 'g_ImplementationHelper' object.


3-3: Create the Extension Configuration Files


Special-Student-7
We are going to register the UNO service via a LibreOffice or Apache OpenOffice extension, and we need some configuration files for that.

Lenard
I didn't know I was going to create an "extension".

Special-Student-7
Sir, it is as easy as creating some configuration files and archiving the ingredients in a ZIP file.

Lenard
I see.

Special-Student-7
We are creating that "UnoServiceComponents.xml" file, like this.

@XML Source Code
<?xml version="1.0" encoding="UTF-8"?>
<components xmlns="http://openoffice.org/2010/uno-components">
	<component loader="com.sun.star.loader.Python" uri="Scripts/python/theBiasPlanet/tests/globalUnoServicesProvider/TestsGlobalUnoServicesProvider.py">
		<!-- # Add UNO component class names -->
		<implementation name="theBiasPlanet.tests.models.PythonTest1UnoComponent.PythonTest1UnoComponent">
			<!-- # Add UNO service names Start -->
			<service name="theBiasPlanet.tests.PythonTest1UnoComponent"/>
			<!-- # Add UNO service names End -->
			<!-- # Add UNO compound interface names Start -->
			<!-- <service name="???"/> -->
			<!-- # Add UNO compound interface names End -->
		</implementation>
	</component>
</components>

Jane
The tag name is "component", but it does not correspond to a UNO component, right?

Special-Student-7
No, it does not. "component" could mean anything in the lax official terminology, and now, "component" seems to mean the set of possibly multiple Python UNO services defined in the extension.

Jane
Anyway, that is how the global UNO services provider is recognized by the office product.

Special-Student-7
Note that the UNO service name should be specified there as well as "service" in the 2nd meaning names should.

Lenard
Huh? As those 2 methods do not need to support the UNO service name, why does the configuration file need the UNO service name?

Special-Student-7
You have to endure such inconsistency; "service" is really jumbled up in the code, not just in the documentation.

Anyway, we need to create also "manifest.xml" of course, but I do not show it here, because it is exactly as in the article.


3-4: An Aside: What Are Not Necessary


Lenard
I need to create the service IDL file, don't I?

Special-Student-7
No, because any service IDL file is for creating a "service" in the 2nd meaning or "service" in the 3rd meaning in the lax official terminology, not for creating a UNO service.

You will never need to create any "service" in the 2nd meaning; you, a Python programmer, cannot use any "service" in the 3rd meaning, because there is no tool for generating the Python image of it, although a Java programmer can use it to instantiate your Python UNO service.

Lenard
But they gives me some peace of mind.

Special-Student-7
Well, why will you not just have the peace of mind without the bother of creating meaningless things?


3-5: Create the Extension File


Special-Student-7
Now, we are going to create the extension file.

I do not need to explain how, because it has been already detailed in a previous article.


3-6: Register the Extension


Special-Student-7
We are going to register the extension.

It can be done via the GUI menu item "Tools" -> "Extension Manager...".

Or you can use the 'unopkg' command, if your build script wants to register the extension, like this.

@bash or cmd Source Code
unopkg add -f -v %the extension file path%

Note that LibreOffice or Apache OpenOffice instance is supposed to have been shut down before the command.


3-7: Setting the Additional Modules Paths into the Office Product


Special-Student-7
While we have created the UNO component and the global UNO services provider in 2 different modules, in fact, that structure requires a configuration, without which the global UNO services provider would not be able to import the UNO component.

In fact, the path of the extension Python base directory is not automatically set into the office product.

Lenard
Huh? Ridiculous! Then, any macro in my extension can't import any module in the same extension!

Special-Student-7
It cannot, without the configuration, as a matter of fact.

Lenard
Then, I have to make the macro self-sufficient, pushing all the depended code into the macro module, foolish!

Special-Student-7
Yes, unless the configuration is in place, as is explained in a section of another article.

Lenard
I think that the configuration should be incorporated into the office product.

Special-Student-7
Please advocate the cause.

If you do not do the configuration, the UNO component will have to be put into the global UNO services provider module.

And if your UNO service uses some libraries made by you or otherwise, the paths of the libraries will have to be set into the office product.

Lenard
Can't I just register the libraries into the Python product itself?

Special-Student-7
You could; for example, any library installed by 'pip' is certainly a part of the Python product, and their paths do not have to be explicitly set into the office product.

But you usually do not register a library made by you into the Python product, do you?

Lenard
Not, if it is not necessary.

Jane
I can copy the libraries into the extension, after the configuration has been in place, can't I?

Special-Student-7
Yes, you can, but as copying the libraries into multiple extensions makes the libraries redundant, I would rather locate the libraries in the local file system and set the paths into the office product by the way explained in another section of the article, letting the libraries be shared by multiple extensions and others whatever.

Jane
Hmm.


4: Instantiate the UNO Service


Special-Student-7
The UNO service can be instantiated, as any built-in global UNO service can.

Jane
Let me see how the above UNO service can be instantiated.

Special-Student-7
For example, this is a Python macro.

@Python Source Code
from typing import Any
from typing import cast
import uno
~
from com.sun.star.script.provider import XScriptContext
from com.sun.star.uno import XComponentContext
XTest1: Any = uno.getClass ("theBiasPlanet.unoDatumTypes.tests.XTest1")

XSCRIPTCONTEXT: XScriptContext
c_underlyingRemoteUnoObjectsContextInXComponentContext: XComponentContext = XSCRIPTCONTEXT.getComponentContext ()
~
c_pythonTest1UnoComponent: "XTest1" = cast (XTest1, c_underlyingRemoteUnoObjectsContextInXComponentContext.getServiceManager ().createInstanceWithArgumentsAndContext ("theBiasPlanet.tests.PythonTest1UnoComponent", ["Hello"], c_underlyingRemoteUnoObjectsContextInXComponentContext))
~

def test1 () ->None:
	~
	c_pythonTest1UnoComponent.test1 ("dude")
	~


Jane
Hmm, the Python class can be instantiated from any remote UNO programming language environment, now.


5: Here Is a Workable Sample


Special-Student-7
Here is a workable sample extension.

How to build the sample project is explained in an article.

The main project is 'hiUnoExtensionsUnoExtension', which uses the additional UNO datum types project and some utility projects.

The UNO datum types merged file has to be registered into the office product according to the way 1) stated in a previous article.

And the paths of the utility Python code base directories ('coreUtilitiesToBeDisclosed/target' and 'unoUtilitiesToBeDisclosed/target') have to be set into the office product according to the way mentioned in a previous section.

Lenard
I do not expect the uneducated users to be able to do such settings, as the extension is supposed to be distributed to such users.

Special-Student-7
You can un-necessitate the setting of the UNO datum types merged file by putting the UNO datum types merged file into the extension by the way 2) stated in a previous article, but the situation concerning the utility libraries is as is stated in the previous section.

Lenard
But it is meaningless if the users can't do the settings.

Special-Student-7
That is a matter of how you prepare an installer that automates the settings, and if your UNO component is self-sufficient, you will not need to worry about libraries.

Lenard
Well, I can prepare the installer, as it has to just copy some files, supposing that the office product directory path is pre-known.

Jane
Your installer can just ask the user the directory path.

Special-Student-7
Anyway, the extension should have registered a tool buttons group, from which the testing macro can be called.

Lenard
"buttons"? Will I get multiple buttons?

Special-Student-7
You will have only one for-Python-UNO-service button; the other 2 buttons are for a Java UNO service and for a C++ UNO service, which will be unusable if you have disabled Java and C++ for building the sample project.


References


<The previous article in this series | The table of contents of this series | The next article in this series>