ApplicationsHow-to

Calling a native image processing DLL from Visual Basic

Call a native C/C++ image processing DLL from Visual Basic with Declare: buffer passing, bitness, calling conventions and a 30 fps throughput worked example.

LumaSpec Vision13 min read Share on LinkedIn

On this page 13 sections
  1. 01Why the pixel loop cannot stay in Visual Basic
  2. 02The split: VB for the GUI, a DLL for the pixels
  3. 03What you need
  4. 04The procedure, end to end
  5. 05Declaring the DLL entry points
  6. 06Passing the frame buffer
  7. 07Sizing the job: a 4000 × 3000 8-bit stream at 30 fps
  8. 08Bitness: match the DLL to the executable
  9. 09Prove the DLL works before it sees a camera
  10. 10Getting frames from a GigE Vision or USB3 Vision camera
  11. 11When plain Visual Basic is enough
  12. 12Pre-build checklist
  13. 13Questions

A Visual Basic program can run a vision station for years, until the code starts walking pixels. A loop that touches every pixel of a multi-megapixel frame is where interpreted VB loses to compiled native code by orders of magnitude, and no faster PC buys that margin back. The proven architecture is a split: keep the user interface, the plant communication and the pass-fail logic in Visual Basic, and move every per-pixel image processing loop into a small native C/C++ DLL that the VB program calls with a Declare statement. This guide gives the Declare syntax and calling conventions, the buffer-passing pattern, bitness matching, a throughput calculation for a 4000 × 3000 camera at 30 fps, and a 4 × 4 test that proves the DLL is correct before it ever sees a live frame.

Why the pixel loop cannot stay in Visual Basic

VB6 executes line by line, checking types and array bounds on every statement. A per-pixel filter runs that machinery once per output pixel, so on a 2 MP frame the interpreter overhead is paid two million times. As an order-of-magnitude rule, a 2 MP grayscale convolution that a compiled DLL finishes in milliseconds takes seconds as pure VB code — a gap of two to three orders of magnitude. The cost scales with pixel count, so a 12 MP frame is six times worse than a 2 MP one.

Compiled code keeps the inner loop in registers with simple stride arithmetic; the interpreter pays a dispatch on every array read and write. Nothing about the camera, the lens or the controller changes this — it is a property of the language runtime. The damage is not only slow cycle times: when processing runs slower than acquisition, frames queue and drop. The fix keeps your existing program and relocates the loop into native code.

The split: VB for the GUI, a DLL for the pixels

Divide the program along the data-size line. Visual Basic owns everything people and machines interact with: buttons, result display, recipes, logging, and messages to PLCs and robots. The DLL owns every function whose input or output is a full frame: filters, thresholding, blob pre-processing, measurement. The VB side passes byte arrays in and receives plain numbers out — coordinates, counts, pass-fail flags — never images.

It is the same pattern other environments use when they need native speed; the guide on calling a camera SDK DLL from LabVIEW with callbacks, buffers and snap shows the identical structure from the measurement side. Plant software does this too: a Visual Basic front end can drive a Siemens MPI DLL to talk to a PLC without OPC on the same machine while native code handles the heavy loops. Keep the boundary narrow — a handful of flat functions taking byte arrays and returning integers — and both sides stay testable.

The host PC deserves the same engineering as the camera. A vision controller puts several camera ports on one industrial board instead of a tower full of USB adapters; the MV-VC3301H-128G66, for example, provides 5+ LAN ports, 4 built-in light channels, 8 GB of memory and an Intel Core i3, so cameras and lights land on the machine that runs your VB program.

Vision controllersHikrobot MV-VC3301H-128G66 I3-8100, 8GB+128G SSD, No PoE, PCIe expansion supported, Incl. VM6200Pro licenseMV-VC3301H-128G66
LAN ports
5+ LAN ports
USB 3 ports
3-4 USB 3 ports
Light outputs
4 built-in channels
Memory
8 GB
$2,419.00View partBuild a system with it

What you need

  • Visual Basic 6 or VB.NET. Note the platform target now: VB6 builds are always 32-bit; VB.NET lets you choose x86, x64 or AnyCPU.
  • A C compiler for the DLL — any toolchain that builds a plain Win32 DLL with exported stdcall or cdecl functions.
  • The camera SDK's native library. GigE Vision and USB3 Vision area scan cameras ship a C/C++ library as standard; note its bitness and calling convention from the SDK manual.
  • The camera, its interface hardware, and one controller or PC with the port count your camera count needs.

The procedure, end to end

  1. Note the SDK library's bitness and calling convention from its manual, and pick your VB flavor: VB6 (always 32-bit) or VB.NET with a fixed platform target matching the DLLs you have.
  2. Write the DLL's pixel functions with fixed-size parameters — integer widths and heights, byte pointers for buffers — and export them with one agreed calling convention.
  3. Declare each export in VB with a Declare statement, adding an Alias clause for any decorated export name shown by a DLL export viewer.
  4. Size the buffers as width × height × bytes per pixel once at start-up, passing stride separately whenever rows may be padded.
  5. Prove the DLL on a 4 × 4 linear ramp before any live frame: a 3 × 3 box filter must return each interior pixel's own value, or your stride or indexing is wrong.
  6. Time 100 identical calls on a full-size buffer, divide by 100, and confirm the per-frame time sits under your frame budget — 33 ms at 30 fps.
  7. Wire up SDK acquisition in continuous mode, declare the SDK functions the same way, and route each filled buffer straight into the DLL.
  8. Divide the link budget across ports when adding cameras, so every stream fits the wire.

The sections below carry out each step with the code and the arithmetic.

Declaring the DLL entry points

The Declare statement maps a DLL export onto a VB function name. Suppose your DLL exposes one box filter — this function name and DLL name are this example's own:

/* imgproc.dll: built as a 32-bit DLL with stdcall exports */
int __stdcall img_box3x3(int width, int height,
                         const unsigned char *src,
                         unsigned char *dst);

The VB6 side declares it like this:

Declare Function img_box3x3 Lib "imgproc.dll" Alias "_img_box3x3@16" ( _
    ByVal width As Long, ByVal height As Long, _
    ByRef src As Byte, ByRef dst As Byte) As Long

The Alias clause is the piece people miss. C compilers default to the cdecl convention and decorate exported names; VB6 calls stdcall and expects plain names. Match one side to the other: build the DLL with stdcall exports and alias the decorated name as above, or keep cdecl and map the underscore-prefixed name instead — the Alias clause exists for exactly this mapping. A mismatch shows up as a DLL entry point error at call time, or a crash just after the function returns, when the two sides disagree about who cleans the stack.

VB.NET's Declare defaults to stdcall as well; for a cdecl export, put a DllImport attribute on a shared function and set its calling convention to Cdecl. Keep parameters to fixed-size types: Long in VB6 for counts and buffer bases, Integer or IntPtr in VB.NET, Byte arrays for image data, Integer return codes.

Passing the frame buffer

Process bytes, not pictures. A picture-box image and a compressed file's bytes are wrappers; the pixel routine needs the flat byte array that the SDK's acquisition buffer already is, described by width, height and stride — the image bytes versus pixel data distinction is the first thing to get straight. Copy the frame into a byte array sized width × height × bytes per pixel and pass the first element:

Dim width As Long, height As Long, ret As Long
width = 4000: height = 3000
Dim src() As Byte, dst() As Byte
ReDim src(width * height - 1)
ReDim dst(width * height - 1)
ret = img_box3x3(width, height, src(0), dst(0))

Passing src(0) ByRef hands over the address of byte 0, and the C side walks src as an ordinary array. Two details decide whether the result comes out straight. Stride: some SDKs pad each row to a 4-byte boundary, so pass the row step as its own parameter whenever it can differ from width. Channel count: a color or Bayer frame carries three bytes per pixel and the buffer arithmetic must multiply by three — the same check as the multi-channel buffer size check before processing. When a detection library is involved the rule repeats: feed it pixel data, not a bitmap object, as in the .NET barcode case.

Sizing the job: a 4000 × 3000 8-bit stream at 30 fps

Inputs, each with its source: the camera is the 12 MP LS-CMR-0166, which delivers 4000 × 3000 frames at 30 fps over USB3 (catalog specification). The pixel format is 8-bit mono at 1 byte per pixel (this example's input assumption — use the format your application actually runs), and the workload is a 3 × 3 filter, which touches every output pixel once.

Area scan cameras12 MP USB3 area scan camera, IMX226, 1/1.7" rolling shutter, 30 fps, 4000 × 3000 resolution (LS-CMR-0166)LS-CMR-0166
Sensor format
1/1.7"
Interface
USB3
Resolution
12 MP
Shutter
Rolling
$560.00View partBuild a system with it

The rule, in words: one frame's size in bytes equals width times height times bytes per pixel, and the stream rate equals frame size times frame rate.

frame size = 4000 px × 3000 px × 1 B/px = 12,000,000 B = 12 MB stream rate = 12 MB/frame × 30 frames/s = 360 MB/s byte pace = 1 s ÷ 360,000,000 B = 2.8 ns per byte wire check = 1 Gbit/s ÷ 8 = 125 MB/s per GigE port USB3 check = 5 Gbit/s ÷ 8 = 625 MB/s, above 360 MB/s

The result: 12 MB per frame and 360 MB/s of payload, a byte arriving every 2.8 ns. The wire checks also explain the interface: 360 MB/s is nearly three times the 125 MB/s one GigE port offers at the wire, while USB3's 625 MB/s carries it with margin.

The decision, with its threshold. Take the pure-VB cost as roughly 2 s for a 2 MP convolution — a round figure from the order-of-magnitude rule above. Pixel work scales linearly with pixel count, so on this frame the same loop costs 12 MP ÷ 2 MP × 2 s = 12 s, against a per-frame budget of 1 s ÷ 30 = 33 ms. That is about 360 times over budget; even if the estimate were ten times optimistic, 1.2 s would still be 36 times over. The pixel loop cannot stay in VB. It moves into the DLL, and the DLL's measured per-frame time must land under 33 ms; Visual Basic keeps the interface, the result handling and the display.

Bitness: match the DLL to the executable

A 32-bit process loads only 32-bit DLLs, and a 64-bit process loads only 64-bit DLLs. VB6 executables are always 32-bit, so both your image DLL and the camera SDK library must be 32-bit builds; on 64-bit Windows they run through the operating system's built-in 32-bit compatibility layer. In VB.NET, set the platform target to match the DLLs you have — x86 with 32-bit libraries, x64 with 64-bit — rather than leaving AnyCPU to decide at run time. The failure is a load error on the first call, before any pixel is touched, and it is the same failure Python developers meet when a wrapper's bitness does not match the interpreter. The fix is identical in both languages: build or select the DLL flavor that matches the executable.

Prove the DLL works before it sees a camera

Hand the DLL a case with a known answer. A 4 × 4 linear ramp is ideal: a 3 × 3 box filter — the sum of the eight neighbors plus the center, divided by nine — returns each interior pixel's own value, because a linear ramp averages back to its center value. Any stride, offset or indexing mistake breaks that identity immediately.

Dim w As Long, h As Long, r As Long, c As Long, ret As Long
w = 4: h = 4
Dim t() As Byte, o() As Byte
ReDim t(w * h - 1): ReDim o(w * h - 1)
For r = 0 To h - 1
    For c = 0 To w - 1
        t(r * w + c) = CByte(r * w + c + 1)
    Next c
Next r
ret = img_box3x3(w, h, t(0), o(0))
' pixel (1,1): neighbors 1,2,3,5,6,7,9,10,11 -> 54 / 9 = 6
' pixel (2,2): neighbors 6,7,8,10,11,12,14,15,16 -> 99 / 9 = 11
If o(1 * w + 1) <> 6 Or o(2 * w + 2) <> 11 Then
    MsgBox "stride, offset or filter is wrong"
End If

The arithmetic checks by hand. Pixel (1, 1) sums its nine-cell neighborhood to 54, and 54 ÷ 9 = 6, its center value. Pixel (2, 2) sums to 99, and 99 ÷ 9 = 11. Both matching means the DLL steps rows with the right stride and divides by the right area. Then time it at real size: wrap a timer around 100 identical calls on a full-size buffer and divide by 100. That per-frame figure is what the 33 ms budget above has to beat.

Getting frames from a GigE Vision or USB3 Vision camera

The camera side follows a fixed pattern in every GigE Vision or USB3 Vision SDK: open the device, allocate one or more capture buffers, start continuous acquisition, and receive filled buffers through a callback or a grab call. Your VB program declares those SDK functions exactly as it declared img_box3x3. The SDK manual prints the exact names, parameter types and calling conventions — put them into the Declare lines as printed, because they differ between SDKs.

Run acquisition in continuous mode rather than re-arming per frame, so the stream stays steady and the buffer queue stays predictable. If the station must change exposure, gain or packet size while running, those settings are register writes through the SDK's control interface, callable from the same VB code. And if a grab call times out while a ping to the camera succeeds, the fault sits in the transport or the buffer queue — check that before suspecting your own declarations.

With more than one camera, divide the link budget first. Each GigE port carries 125 MB/s at the wire, so the 12 MB frames of the worked example cap at 125 ÷ 12 ≈ 10 fps per port. Controllers with 5+ LAN ports exist for exactly this arithmetic.

When plain Visual Basic is enough

Three cases justify skipping the DLL. Small regions: a 640 × 480 ROI is 640 × 480 = 307,200 bytes, about 0.3 MB, and once per second that is trivial for interpreted code. Rare operations: a calibration snapshot or a manual operator check has no cycle budget to miss. Existing SDK detection: when the SDK's own library already finds the barcode or the code, VB should call it and read the result rather than reimplement it.

The MV-CU020-90GC is a 2.5 MP GigE camera with a global shutter, the kind of camera many first VB integrations start on. Its 8-bit frames run 2.5 MB, so 30 fps draws 2.5 MB × 30 = 75 MB/s of a GigE port's 125 MB/s wire capacity — the link is comfortable. The per-pixel work is what still needs the DLL at that rate: 75 MB/s of byte visits is beyond an interpreter, by the same arithmetic as the worked example.

Area scan camerasHikrobot MV-CU020-90GC 2.5 MP Area Scan Camera, GigE, GMAX4002, ColorMV-CU020-90GC
Sensor format
1/1.7"
Lens mount
C-mount
Interface
GigE
Resolution
Up to 3 MP
$309.00View partBuild a system with it

Pre-build checklist

  • SDK DLL bitness and calling convention noted from the SDK manual — every Declare line and the platform target depend on them.
  • Platform target matched: VB6 builds 32-bit only; VB.NET set to x86 or x64 to match the DLLs you have.
  • Export names read from a DLL export viewer, with Alias clauses for any decorated names.
  • Buffers sized width × height × bytes per pixel, with stride passed separately where rows may be padded.
  • 4 × 4 ramp test passing: interior outputs 6 at index 5 and 11 at index 10.
  • Timing test done: 100 calls averaged, per-frame time under the frame budget — 33 ms at 30 fps.
  • Link budget divided: each camera's MB/s against the 125 MB/s wire capacity of a GigE port.
  • Controller chosen for LAN port count and built-in light channels first, so cameras and lights all land on it.

To pick the machine that runs all of this, browse the vision controllers and filter them by LAN port count, built-in light channels and processor, or use the camera finder to filter cameras by interface, resolution and shutter. More integration guides sit on the applications topic page, and the contact page reaches the applications team when you want a second opinion on a specific build.

Questions

Can a Visual Basic 6 program load a 64-bit DLL?

No. A VB6 executable always runs as a 32-bit process, and a 32-bit process can only load 32-bit DLLs — so both your image library and the camera SDK library have to be 32-bit builds, even on 64-bit Windows. In VB.NET you set the platform target instead: an x86 build takes 32-bit DLLs, an x64 build takes 64-bit ones. The operating system's bitness does not decide this; the executable's does.

Do I have to copy each SDK frame into a VB array before calling the DLL?

No, and at 12 MB per frame you should not. If the SDK's callback hands your code a buffer address, declare that parameter as a pointer-sized integer (Long in 32-bit VB6, IntPtr in VB.NET) and pass it straight to the DLL, which then works on the SDK's own memory. Copy into a VB array only when VB code itself must touch the pixels, for drawing overlays for example, and reuse that array instead of reallocating.

The entry point is not found, or the call crashes — what do I check first?

Three things, in order. The exported name: many compilers decorate stdcall exports with a leading underscore and the parameter byte count, which the Alias clause maps. The calling convention: stdcall and cdecl must agree, or the stack corrupts on return. The buffer size: width × height × bytes per pixel, dimensioned before the call. A DLL export viewer shows the decorated names to match against your Declare line.

How much faster is the native DLL than the VB loop?

On per-pixel loops, expect two to three orders of magnitude: work measured in milliseconds compiled versus seconds interpreted, growing with pixel count because the cost scales linearly. Measure your own pair of numbers rather than trusting a rule: time 100 identical calls on the same full-size buffer on both the VB side and the DLL side, divide each by 100, and compare against your frame budget — 33 ms at 30 fps. To estimate a different resolution from one measurement, double the pixels, double the time.