/***************************************************************************************************
Copyright (C) 2025 The Qt Company Ltd.
SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
***************************************************************************************************/
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Reflection.PortableExecutable;
namespace Test_Qt.DotNet.Generator.Support
{
internal sealed class AssemblyConfig
{
internal string AssemblyName { get; init; }
internal string TypeName { get; init; }
internal string MethodName { get; init; }
}
///
/// Configuration for the method's return value. By overriding ReturnIl, you can
/// provide IL instructions to compute and push the return value onto the evaluation stack
/// before emitting ret.
///
internal sealed class ReturnConfig
{
internal Action EncodeType { get; init; }
internal Action ReturnIl { get; init; } = il => il.OpCode(ILOpCode.Ret);
}
///
/// Configuration for the method's parameters.
///
internal sealed class ParameterConfig
{
internal Action EncodeTypes { get; init; }
internal IReadOnlyList Names { get; init; } = [];
}
///
/// Dynamically builds an in-memory .NET assembly containing a single static method with a
/// specified return type, the given parameters, and parameter names.
/// By setting includeParamRows to false, produces a lean assembly with no
/// Param table entries, while using the default true adds Param rows for the return
/// value and each argument, making names and attributes visible in metadata.
///
internal static class InMemoryAssemblyBuilder
{
internal static void Build(AssemblyConfig assemblyConfig, ReturnConfig returnConfig,
ParameterConfig parameterConfig, Stream outputStream, bool includeParamRows = true)
{
ArgumentNullException.ThrowIfNull(assemblyConfig);
ArgumentException.ThrowIfNullOrEmpty(assemblyConfig.AssemblyName);
ArgumentException.ThrowIfNullOrWhiteSpace(assemblyConfig.TypeName);
ArgumentException.ThrowIfNullOrWhiteSpace(assemblyConfig.MethodName);
ArgumentNullException.ThrowIfNull(returnConfig);
ArgumentNullException.ThrowIfNull(returnConfig.EncodeType);
ArgumentNullException.ThrowIfNull(parameterConfig);
var paramCount = parameterConfig.Names.Count;
if (paramCount > 0)
ArgumentNullException.ThrowIfNull(parameterConfig.EncodeTypes);
// Initialize metadata builder
var metadataBuilder = new MetadataBuilder();
// Add module
_ = metadataBuilder.AddModule(
generation: 0,
moduleName: metadataBuilder.GetOrAddString(assemblyConfig.AssemblyName + ".dll"),
mvid: metadataBuilder.GetOrAddGuid(Guid.NewGuid()),
encId: metadataBuilder.GetOrAddGuid(Guid.NewGuid()),
encBaseId: metadataBuilder.GetOrAddGuid(Guid.NewGuid()));
// Add assembly
_ = metadataBuilder.AddAssembly(
name: metadataBuilder.GetOrAddString(assemblyConfig.AssemblyName),
version: new Version(1, 0, 0, 0),
culture: default,
publicKey: default,
flags: 0,
hashAlgorithm: AssemblyHashAlgorithm.None);
var sysRuntimeName = Assembly.Load("System.Runtime").GetName();
ArgumentException.ThrowIfNullOrEmpty(sysRuntimeName.Name);
ArgumentNullException.ThrowIfNull(sysRuntimeName.Version);
// Public key token must be present to unify identities
var pkt = sysRuntimeName.GetPublicKeyToken();
BlobHandle publicKeyOrToken = default;
if (pkt is { Length: > 0 })
publicKeyOrToken = metadataBuilder.GetOrAddBlob(pkt);
// Optional: culture if available
StringHandle culture = default;
if (!string.IsNullOrEmpty(sysRuntimeName.CultureName))
culture = metadataBuilder.GetOrAddString(sysRuntimeName.CultureName);
// Add the AssemblyRef for System.Runtime
var systemRuntimeRef = metadataBuilder.AddAssemblyReference(
name: metadataBuilder.GetOrAddString(sysRuntimeName.Name),
version: sysRuntimeName.Version,
culture: culture,
publicKeyOrToken: publicKeyOrToken,
flags: 0,
hashValue: default);
// Build method signature using the provided configurations
var signatureBuilder = new BlobBuilder();
var signatureEncoder = new BlobEncoder(signatureBuilder);
signatureEncoder.MethodSignature(isInstanceMethod: false)
.Parameters(paramCount, returnConfig.EncodeType, p =>
{
if (paramCount > 0)
parameterConfig.EncodeTypes(p);
});
// IL body
var instructionEncoder = new InstructionEncoder(new BlobBuilder());
returnConfig.ReturnIl(instructionEncoder); // default: just 'ret'
var ilBuilder = new BlobBuilder();
var methodBodyStreamEncoder = new MethodBodyStreamEncoder(ilBuilder);
var methodBodyHandle = methodBodyStreamEncoder.AddMethodBody(instructionEncoder);
// Optional: Add parameter rows (must be added before the method)
ParameterHandle firstParameterHandle;
switch (includeParamRows) {
case true when paramCount > 0:
// Sequence 0 = return parameter
firstParameterHandle = metadataBuilder.AddParameter(
attributes: ParameterAttributes.None,
name: default,
sequenceNumber: 0);
// Add parameters for the method
var paramSequence = 1;
foreach (var paramName in parameterConfig.Names) {
var nameHandle = string.IsNullOrEmpty(paramName)
? default
: metadataBuilder.GetOrAddString(paramName);
_ = metadataBuilder.AddParameter(
attributes: ParameterAttributes.None,
name: nameHandle,
sequenceNumber: paramSequence++);
}
break;
case true when paramCount == 0:
firstParameterHandle = default; // omit Param table entries entirely
break;
default:
var nextParamRow = metadataBuilder.GetRowCount(TableIndex.Param) + 1;
firstParameterHandle = paramCount == 0
? default
: MetadataTokens.ParameterHandle(nextParamRow);
break;
}
// Order here is important
// Compute starting row indices before emitting TypeDefs
var nextFieldRow = metadataBuilder.GetRowCount(TableIndex.Field) + 1;
var nextMethodRow = metadataBuilder.GetRowCount(TableIndex.MethodDef) + 1;
// type (no base type, no fields, no methods)
_ = metadataBuilder.AddTypeDefinition(
attributes: 0,
@namespace: default,
name: metadataBuilder.GetOrAddString(""),
baseType: default,
fieldList: MetadataTokens.FieldDefinitionHandle(nextFieldRow),
methodList: MetadataTokens.MethodDefinitionHandle(nextMethodRow));
// Split "Namespace.Type" into namespace + simple name
var (ns, simpleName) = SplitNamespaceAndName(assemblyConfig.TypeName);
var systemObjectTypeRef = metadataBuilder.AddTypeReference(systemRuntimeRef,
metadataBuilder.GetOrAddString("System"), metadataBuilder.GetOrAddString("Object"));
// User type, it will own methods starting at nextMethodRow
_ = metadataBuilder.AddTypeDefinition(
attributes: TypeAttributes.Class | TypeAttributes.Public
| TypeAttributes.AutoLayout | TypeAttributes.BeforeFieldInit,
@namespace: string.IsNullOrEmpty(ns) ? default : metadataBuilder.GetOrAddString(ns),
name: metadataBuilder.GetOrAddString(simpleName),
baseType: systemObjectTypeRef,
fieldList: MetadataTokens.FieldDefinitionHandle(nextFieldRow),
methodList: MetadataTokens.MethodDefinitionHandle(nextMethodRow));
// Emit the MethodDef(s) that belong to the user type
_ = metadataBuilder.AddMethodDefinition(
MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig,
implAttributes: MethodImplAttributes.IL,
name: metadataBuilder.GetOrAddString(assemblyConfig.MethodName),
signature: metadataBuilder.GetOrAddBlob(signatureBuilder),
bodyOffset: methodBodyHandle,
parameterList: firstParameterHandle);
// Build PE
var peHeaderBuilder = new PEHeaderBuilder(
imageCharacteristics: Characteristics.Dll);
var metadataRootBuilder = new MetadataRootBuilder(metadataBuilder);
var managedPeBuilder = new ManagedPEBuilder(
header: peHeaderBuilder,
metadataRootBuilder: metadataRootBuilder,
ilStream: ilBuilder,
strongNameSignatureSize: 0,
flags: CorFlags.ILOnly);
// Write into the specified stream
var peBlobBuilder = new BlobBuilder();
managedPeBuilder.Serialize(peBlobBuilder);
peBlobBuilder.WriteContentTo(outputStream);
}
private static (string Namespace, string Name) SplitNamespaceAndName(string fullTypeName)
{
if (string.IsNullOrWhiteSpace(fullTypeName))
return ("", "");
// Split only on the last dot so we keep nested markers (e.g., "Outer+Inner")
var lastDot = fullTypeName.LastIndexOf('.');
if (lastDot < 0)
return ("", fullTypeName);
var ns = fullTypeName[..lastDot];
var name = fullTypeName[(lastDot + 1)..]; // may contain '+' for nested types
return (ns, name);
}
}
}