Turning a script package into a DLL without breaking everything

Hey all.

My (very small) team and I over the years have been working on a library that supports a number of games we’ve produced. We incorporate this library as a package through UPM via our in-house Gitlab instance. The library is still evolving, so it also changes from time-to-time as we work. Generally this setup has worked for us.

In our latest project, we are working on a demo with an external customer that has provided the game design. We have nearly finished the project, but corporate has made it clear that our library is not to be included as source code when we share our work with this customer. So I am investigating how we can create a DLL for this purpose.

Making the DLL is the easy part (Visual Studio 2022). It’s when I swap it into the project I end up breaking the links for every prefab and ScriptableObject that is included with the library. This makes sense because inside the editor those objects are referencing their scripts, not the DLL, but trying to manually swap the references or create replacements is a real pain.

Does anyone have a workflow and/or tool(s) they can suggest to make this task easier?

Hi!

Maybe I’m wrong about this but a .dll file is a compiled dynamic library so you don’t really have access to the scripts as component after it is compiled. Usually what we do is to have generic code in the .dll files that is then called from scripts that are used as components.

I’ve never faced the case when someone tries to extract a script from a .dll to turn it into a component and I’m not sure that this is technically possible.

I’m not trying to extract scripts from the DLL.

When the DLL is swapped in for our Git package, the GUIDs are getting changed out on anything we’ve already created. This causes all our prefabs and in-editor GameObjects to report “Missing” scripts.

The issue is how can we transition to a DLL and also migrate the GUID references without a lot of manual labor.

Years ago when Unity took certain Asset Store projects in-house and began delivering them as DLL packages they had special scripts that remapped all the GUIDs in the meta files in your project to reconnect things to the new DLL GUIDs for each class.

I think this might be necessary to get your MonoBehaviour and ScriptableObjects reconnected…

I think they did this in TextMeshPRO and ProBuilder… here’s the TMPro one:

4 Likes

Also, be aware that compiling a DLL does not prevent anyone from reflecting into it to view the source code. You could use some kind of obfuscation, but that doesn’t truly prevent it, it just creates an additional obstacle. If their computer is capable of decrypting the DLL to make use of it, then so are they if they really want to. Also, an obfuscated DLL will have a performance overhead for decryption. However, I can understand the feeling that a DLL provides a layer of abstraction that can have a copyright notice explicitly applied to the whole container, if you feel it’s important. Copyright is guaranteed with or without a notice though. If someone spreads copies of your code without the proper license, it is dealt with in court. If you feel you have a patentable concept then you would protect that with a patent. In any case, a DLL will not truly hide the source code, it’s more like building a wall around it, but there is always a way through. Even if the gate is locked, the key is also on their computer somewhere.

3 Likes

This is good advice for anyone faced with a similar situation.

In our case we are working with a government entity that has to abide by strict rules of conduct, so no worries they will do something unethical. It’s really all about meeting a legal requirement. They want that layer of obfuscation until we are at a point where we can share the source code with our partners in the future.

2 Likes

Yes! This sounds like its exactly the sort of utility we need, albeit for our own stuff. It would be great if the code for it was available somewhere so we could create our own in-house remapping tool.

1 Like

The GUID Remapping Tool (TMP_ProjectConversionUtility) can be found inside the TMP_PackageUtilities.cs file in the TextMeshPro package.

3 Likes

You probably know this having worked with your library as-is for this much time, but let me explicitly state and warn you: Do not change your team over to a DLL-based workflow.

I would not even convert that project to use DLLs if you intend to do more work on it.

In other words, do this repackage / rework ideally only in the moment before you cut a source drop for your customer, then revert all the changes once you have tested things.

DLLs are that much of a horrible thing in Unity3D workflow.

Here’s more reading on the nightmare of brittleness you get with DLLs:

1 Like

Oh, I’ve already dipped my toes in this murky pool enough to know I don’t wanna deal with DLLs if I don’t have to. :stuck_out_tongue_winking_eye: Unfortunately, I don’t have a better solution at the moment.

That’s why I was hoping for a tool that we could grind out a DLL when needed as opposed to creating a separate workflow to maintain one. The TextMeshPro approach looked enticing, but the repo file linked above for mapping between DLLs and source calls a method that is commented out and not included. (Did a search over the whole repo for the method and it’s not present except where the commented out line would’ve called it).

Are you talking about the calls to ConvertGUIDFromDLLToSource() and ConvertGUIDFromSourceToDLL() ?

I don’t think you need those. I’m guessing they just produce the remap files.

The whole thing just looks like a glorified purpose-built sed script driven by that asset remapping table.

I think you just need to make your own version of the remap files, eg, this file:

./Library/PackageCache/com.unity.textmeshpro@1.4.1/PackageConversionData.json

which is just the table of remaps. You would not benefit from having the ones from TMPro.

// looks like:
        {
            "referencedResource": "TMP_SubMesh.cs",
            "target": "guid: bd950677b2d06c74494b1c1118584fff",
            "replacement": "guid: 07994bfe8b0e4adb97d706de5dea48d5"
        },
		{
            "referencedResource": "TMP_SubMesh.cs",
            "target": "fileID: 1330537494, guid: 89f0137620f6af44b9ba852b4190e64e",
            "replacement": "fileID: 11500000, guid: 07994bfe8b0e4adb97d706de5dea48d5"
        },

Disclaimer: haven’t actually tried doing it, but it should in theory work, and I have hand-renamed a metric buttload of classes in my day… :slight_smile:

1 Like

This might help

1 Like

Hey, I can see there’s an opportunity here to talk a bit more in depth about why your GUIDs get lost when you use a DLL instead of the C# scripts.

Since each C# script has a corresponding .meta file, there will be a GUID associated to it.

For example, if I have a Prefab with a reference to a MonoBehaviour called Foo01, it will generate the following:

Foo01GO.prefab

%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &363824360130945602
GameObject:
...
--- !u!4 &331547713739031506
Transform:
...
--- !u!114 &8569672213745730772
MonoBehaviour:
...
m_Script: {fileID: 11500000, guid: 5e003a5623c777545aadc493adfc4ef5, type: 3}
...

Foo01.cs.meta:

fileFormatVersion: 2
guid: 5e003a5623c777545aadc493adfc4ef5

If we then compile Foo01 into a DLL (via an Assembly Definition), then Foo01 would live inside the DLL (and get compiled into $project/Library/ScriptAssemblies/AssemblyTesting.dll in this case)

If you notice on the Project Browser, the DLL actually has “Assets” which look like scripts.

Note: Having the source code & the assembly in the same project will throw an error:

Plugin ‘Assets/Testing/Assembly/AssemblyTesting.dll’ has the same filename as Assembly Definition File ‘Assets/Testing/Scripts/AssemblyTesting.asmdef’. Rename the assemblies to avoid hard to diagnose issues and crashes.

Next, assign Foo01 from the DLL into your Prefab, and now your Prefab would refer to another GUID and a different fileId:

Foo01GO.prefab:

%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &363824360130945602
GameObject:
...
--- !u!4 &331547713739031506
Transform:
...
--- !u!114 &8569672213745730772
MonoBehaviour:
...
m_Script: {fileID: -7776210, guid: b51bcd74f73a4e44c8c4a24a6fc1cab9, type: 3}
...

In this case, guid: b51bcd74f73a4e44c8c4a24a6fc1cab9 refers to AssemblyTesting.dll that you’ve added to the project.

So, that’s how you’d do it manually.

If you want to automate it, you could do the following:

Use AssetDatabase.LoadAllAssetsAtPath, for the DLL you’ll get a list of assets, which are MonoScripts that you can then extract the class from.

[MenuItem("AssetDatabase/LoadAllAssets")]
public static void LoadAllAssets()
{
    var path = "Assets/Testing/Assembly/AssemblyTesting.dll";
    var assets = AssetDatabase.LoadAllAssetsAtPath(path);

    foreach (var asset in assets)
    {
        var monoScript = asset as MonoScript;
        AssetDatabase.TryGetGUIDAndLocalFileIdentifier(asset, out var guid, out var localId);
        Debug.Log($"Name: {asset.name}, GUID: {guid}, ID: {localId}, type: {asset.GetType()}, class:  {monoScript.GetClass().FullName}");
    }
}

Output:
Name: Foo02, GUID: b51bcd74f73a4e44c8c4a24a6fc1cab9, ID: 1441409801, type: UnityEditor.MonoScript, class: AssemblyTesting.Foo02

Name: Foo01, GUID: b51bcd74f73a4e44c8c4a24a6fc1cab9, ID: -7776210, type: UnityEditor.MonoScript, class: AssemblyTesting.Foo01

After you have that, you could do something similar to what TextMeshPro does, and create a remapping table (you’ll need the original GUID & fileIds aswell), such that you can update the GUID and fileId in your .meta files and automate this. You’ll need need to patch up the .meta files so you can overwrite the old references with the new ones.

Note: Since the .meta fie for the C# scripts doesn’t contain a fileId, you should be able to rely on fileId:11500000 being the fileId that refers to scripts.

All in all, this should let you get started on writing a conversion tool and then ship a project with a DLL version of your code :slightly_smiling_face:

1 Like

Thanks for the insight Javier, that’s very useful.
I wonder if there is a way to generate or import the DLL in such way that the GUIDs match.

You could make the GUIDs match, if you edit the GUID in both the Prefab file and the .meta file. You would also have to delete the original script, because its GUID will be registered to the AssetDatabase and if there are 2 assets with the same GUID, one of them will have its GUID changed to prevent collisions (you can reproduce this using Windows Explorer by duplicating a folder that’s already in your project and then going back to Unity so it’ll pick up the changes. You should see a warning about GUIDs then).

Additinally, you’d also need to make sure the FileID matches, since that also changed in the example above, so it would be quite a manuall process. On top of that, any Asset that refers to the original script’s GUID will have to also be updated with the correct FileID. So, even though it may seem possible to do, a number of edge cases can arise making this GUID switching approach more trouble than its worth.

1 Like

Yes, that was the same conclusion I arrived to.

I want my package to offer two tiers: source and DLL. So, my solution was to separate Unity scripts like MonoBehavior implementations, from the other “core” classes, move as much as I can to this core, and set it as a global .asmdef that can in turn just be replaced by the corresponding DLL. No GUID reassigning needed.

So far it works, the obvious problem is having to adapt the architecture a bit.

1 Like